tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from datetime import datetime, timedelta
  41from dateutil.tz import tzlocal, tzutc
  42from time import sleep
  43
  44import re
  45import json
  46import requests
  47import traceback as tb
  48from typing import Union
  49
  50from multiprocessing import cpu_count
  51from multiprocessing.pool import ThreadPool
  52import pandas as pd
  53
  54from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  55from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  56
  57from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator
  58from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  59
  60import UniLogger as uLog  # Logger for TKSBrokerAPI
  61
  62
  63# --- Common technical parameters:
  64
  65PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  66uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  67uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  68uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  69
  70__version__ = "1.5"  # The "major.minor" version setup here, but build number define at the build-server only
  71
  72CPU_COUNT = cpu_count()  # host's real CPU count
  73CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  74
  75
  76class TinkoffBrokerServer:
  77    """
  78    This class implements methods to work with Tinkoff broker server.
  79
  80    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  81
  82    About `token`: https://tinkoff.github.io/investAPI/token/
  83    """
  84    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  85        """
  86        Main class init.
  87
  88        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  89        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  90                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  91        :param useCache: use default cache file with raw data to use instead of `iList`.
  92                         True by default. Cache is auto-update if new day has come.
  93                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  94        :param defaultCache: path to default cache file. `dump.json` by default.
  95        """
  96        if token is None or not token:
  97            try:
  98                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
  99                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 100
 101            except KeyError:
 102                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 103                raise Exception("Token required")
 104
 105        else:
 106            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 107            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 108
 109        if accountId is None or not accountId:
 110            try:
 111                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 112                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 113
 114            except KeyError:
 115                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 116
 117        else:
 118            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 119            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 120
 121        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 122        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 123
 124        Latest version: https://pypi.org/project/tksbrokerapi/
 125        """
 126
 127        self.aliases = TKS_TICKER_ALIASES
 128        """Some aliases instead official tickers.
 129
 130        See also: `TKSEnums.TKS_TICKER_ALIASES`
 131        """
 132
 133        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 134
 135        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 136
 137        self.ticker = ""
 138        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 139
 140        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 141        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 142
 143        See also: `SearchByTicker()`, `SearchInstruments()`.
 144        """
 145
 146        self.figi = ""
 147        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 148
 149        See also: `SearchByFIGI()`, `SearchInstruments()`.
 150        """
 151
 152        self.depth = 1
 153        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 154
 155        See also: `GetCurrentPrices()`.
 156        """
 157
 158        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 159        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 160
 161        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 162        """
 163
 164        uLogger.debug("Broker API server: {}".format(self.server))
 165
 166        self.timeout = 15
 167        """Server operations timeout in seconds. Default: `15`.
 168
 169        See also: `SendAPIRequest()`.
 170        """
 171
 172        self.headers = {
 173            "Content-Type": "application/json",
 174            "accept": "application/json",
 175            "Authorization": "Bearer {}".format(self.token),
 176            "x-app-name": "Tim55667757.TKSBrokerAPI",
 177        }
 178        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 179
 180        See also: `SendAPIRequest()`.
 181        """
 182
 183        self.body = None
 184        """Request body which send to broker server. Default: `None`.
 185
 186        See also: `SendAPIRequest()`.
 187        """
 188
 189        self.moreDebug = False
 190        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 191
 192        self.historyFile = None
 193        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 194
 195        See also: `History()`.
 196        """
 197
 198        self.htmlHistoryFile = "index.html"
 199        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 200
 201        See also: `ShowHistoryChart()`.
 202        """
 203
 204        self.instrumentsFile = "instruments.md"
 205        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 206
 207        See also: `ShowInstrumentsInfo()`.
 208        """
 209
 210        self.searchResultsFile = "search-results.md"
 211        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 212
 213        See also: `SearchInstruments()`.
 214        """
 215
 216        self.pricesFile = "prices.md"
 217        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 218
 219        See also: `GetListOfPrices()`.
 220        """
 221
 222        self.infoFile = "info.md"
 223        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 224
 225        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 226        """
 227
 228        self.bondsXLSXFile = "ext-bonds.xlsx"
 229        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 230        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 231
 232        See also: `ExtendBondsData()`.
 233        """
 234
 235        self.calendarFile = "calendar.md"
 236        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 237        
 238        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 239
 240        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 241        """
 242
 243        self.overviewFile = "overview.md"
 244        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 245
 246        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 247        """
 248
 249        self.overviewDigestFile = "overview-digest.md"
 250        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 251
 252        See also: `Overview()` with parameter `details="digest"`.
 253        """
 254
 255        self.overviewPositionsFile = "overview-positions.md"
 256        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 257
 258        See also: `Overview()` with parameter `details="positions"`.
 259        """
 260
 261        self.overviewOrdersFile = "overview-orders.md"
 262        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 263
 264        See also: `Overview()` with parameter `details="orders"`.
 265        """
 266
 267        self.overviewAnalyticsFile = "overview-analytics.md"
 268        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 269
 270        See also: `Overview()` with parameter `details="analytics"`.
 271        """
 272
 273        self.overviewBondsCalendarFile = "overview-calendar.md"
 274        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 275
 276        See also: `Overview()` with parameter `details="calendar"`.
 277        """
 278
 279        self.reportFile = "deals.md"
 280        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 281
 282        See also: `Deals()`.
 283        """
 284
 285        self.withdrawalLimitsFile = "limits.md"
 286        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 287
 288        See also: `OverviewLimits()` and `RequestLimits()`.
 289        """
 290
 291        self.userInfoFile = "user-info.md"
 292        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 293
 294        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 295        """
 296
 297        self.userAccountsFile = "accounts.md"
 298        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 299
 300        See also: `OverviewAccounts()`, `RequestAccounts()`.
 301        """
 302
 303        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 304        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 305
 306        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 307
 308        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 309        """
 310
 311        self.iList = None  # init iList for raw instruments data
 312        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 313        
 314        See also: `Listing()`, `DumpInstruments()`.
 315        """
 316
 317        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 318        if useCache:
 319            if os.path.exists(self.iListDumpFile):
 320                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 321                curTime = datetime.now(tzutc())
 322
 323                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 324                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 325
 326                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 327
 328                else:
 329                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 330
 331                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 332                        os.path.abspath(self.iListDumpFile),
 333                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 334                    ))
 335
 336            else:
 337                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 338                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 339
 340        else:
 341            self.iList = self.Listing()  # request new raw instruments data from broker server
 342            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 343
 344        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 345        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 346
 347        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 348        """
 349
 350    def _ParseJSON(self, rawData="{}") -> dict:
 351        """
 352        Parse JSON from response string.
 353
 354        :param rawData: this is a string with JSON-formatted text.
 355        :return: JSON (dictionary), parsed from server response string.
 356        """
 357        responseJSON = json.loads(rawData) if rawData else {}
 358
 359        if self.moreDebug:
 360            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 361
 362        return responseJSON
 363
 364    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 365        """
 366        Send GET or POST request to broker server and receive JSON object.
 367
 368        self.header: must be defining with dictionary of headers.
 369        self.body: if define then used as request body. None by default.
 370        self.timeout: global request timeout, 15 seconds by default.
 371        :param url: url with REST request.
 372        :param reqType: send "GET" or "POST" request. "GET" by default.
 373        :param retry: how many times retry after first request if an 5xx server errors occurred.
 374        :param pause: sleep time in seconds between retries.
 375        :return: response JSON (dictionary) from broker.
 376        """
 377        if reqType not in ("GET", "POST"):
 378            uLogger.error("You can define request type: 'GET' or 'POST'!")
 379            raise Exception("Incorrect value")
 380
 381        if self.moreDebug:
 382            uLogger.debug("Request parameters:")
 383            uLogger.debug("    - REST API URL: {}".format(url))
 384            uLogger.debug("    - request type: {}".format(reqType))
 385            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 386            uLogger.debug("    - body:\n{}".format(self.body))
 387
 388        # fast hack to avoid all operations with some tickers/FIGI
 389        responseJSON = {}
 390        oK = True
 391        for item in self.exclude:
 392            if item in url:
 393                if self.moreDebug:
 394                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 395
 396                oK = False
 397                break
 398
 399        if oK:
 400            counter = 0
 401            response = None
 402            errMsg = ""
 403
 404            while not response and counter <= retry:
 405                if reqType == "GET":
 406                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 407
 408                if reqType == "POST":
 409                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 410
 411                if self.moreDebug:
 412                    uLogger.debug("Response:")
 413                    uLogger.debug("    - status code: {}".format(response.status_code))
 414                    uLogger.debug("    - reason: {}".format(response.reason))
 415                    uLogger.debug("    - body length: {}".format(len(response.text)))
 416                    uLogger.debug("    - headers:\n{}".format(response.headers))
 417
 418                # Server returns some headers:
 419                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 420                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 421                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 422                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 423                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 424                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 425                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 426                    sleep(rateLimitWait)
 427
 428                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 429                if 400 <= response.status_code < 500:
 430                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 431                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 432                    counter = retry + 1
 433
 434                if 500 <= response.status_code < 600:
 435                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 436                    uLogger.debug("    - not oK, {}".format(errMsg))
 437                    counter += 1
 438
 439                    if counter <= retry:
 440                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 441                        sleep(pause)
 442
 443            responseJSON = self._ParseJSON(rawData=response.text)
 444
 445            if errMsg:
 446                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 447                uLogger.error("    - not oK, {}".format(errMsg))
 448
 449        return responseJSON
 450
 451    def _IUpdater(self, iType: str) -> tuple:
 452        """
 453        Request instrument by type from server. See available API methods for instruments:
 454        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 455        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 456        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 457        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 458        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 459
 460        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 461        :return: tuple with iType name and list of available instruments of current type for defined user token.
 462        """
 463        result = []
 464
 465        if iType in TKS_INSTRUMENTS:
 466            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 467
 468            # all instruments have the same body in API v2 requests:
 469            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 470            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 471            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 472
 473        return iType, result
 474
 475    def _IWrapper(self, kwargs):
 476        """
 477        Wrapper runs instrument's update method `_IUpdater()`.
 478        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 479        """
 480        return self._IUpdater(**kwargs)
 481
 482    def Listing(self) -> dict:
 483        """
 484        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 485
 486        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 487        """
 488        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 489        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 490
 491        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 492        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 493        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 494
 495        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 496        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 497        poolUpdater.close()
 498
 499        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 500        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 501        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 502
 503        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 504        for iType in iList.keys():
 505            for ticker in iList[iType]:
 506                iList[iType][ticker]["type"] = iType
 507
 508                if "minPriceIncrement" in iList[iType][ticker].keys():
 509                    iList[iType][ticker]["step"] = NanoToFloat(
 510                        iList[iType][ticker]["minPriceIncrement"]["units"],
 511                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 512                    )
 513
 514                else:
 515                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 516
 517        return iList
 518
 519    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 520        """
 521        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 522
 523        See also: `DumpInstruments()`, `Listing()`.
 524
 525        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 526                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 527        """
 528        if self.iListDumpFile is None or not self.iListDumpFile:
 529            uLogger.error("Output name of dump file must be defined!")
 530            raise Exception("Filename required")
 531
 532        if not self.iList or forceUpdate:
 533            self.iList = self.Listing()
 534
 535        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 536
 537        # Save as XLSX with separated sheets for every type of instruments:
 538        with pd.ExcelWriter(
 539                path=xlsxDumpFile,
 540                date_format=TKS_DATE_FORMAT,
 541                datetime_format=TKS_DATE_TIME_FORMAT,
 542                mode="w",
 543        ) as writer:
 544            for iType in TKS_INSTRUMENTS:
 545                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 546                df = df[sorted(df)]  # sorted by column names
 547                df = df.applymap(
 548                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 549                    na_action="ignore",
 550                )  # converting numbers from nano-type to float in every cell
 551                df.to_excel(
 552                    writer,
 553                    sheet_name=iType,
 554                    encoding="UTF-8",
 555                    freeze_panes=(1, 1),
 556                )  # saving as XLSX-file with freeze first row and column as headers
 557
 558        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 559
 560    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 561        """
 562        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 563        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 564
 565        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 566
 567        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 568                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 569        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 570        """
 571        if self.iListDumpFile is None or not self.iListDumpFile:
 572            uLogger.error("Output name of dump file must be defined!")
 573            raise Exception("Filename required")
 574
 575        if not self.iList or forceUpdate:
 576            self.iList = self.Listing()
 577
 578        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 579        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 580            fH.write(jsonDump)
 581
 582        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 583
 584        return jsonDump
 585
 586    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 587        """
 588        Show information about one instrument defined by json data and prints it in Markdown format.
 589
 590        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 591
 592        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 593        :param show: if `True` then also printing information about instrument and its current price.
 594        :return: multilines text in Markdown format with information about one instrument.
 595        """
 596        splitLine = "|                                                             |                                                        |\n"
 597        infoText = ""
 598
 599        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 600            info = [
 601                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 602                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 603                "| Parameters                                                  | Values                                                 |\n",
 604                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 605                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 606                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 607            ]
 608
 609            if "sector" in iJSON.keys() and iJSON["sector"]:
 610                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 611
 612            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 613                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 614
 615            info.extend([
 616                splitLine,
 617                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 618                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 619            ])
 620
 621            if "isin" in iJSON.keys() and iJSON["isin"]:
 622                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 623
 624            if "classCode" in iJSON.keys():
 625                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 626
 627            info.extend([
 628                splitLine,
 629                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 630                splitLine,
 631                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 632                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 633                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 634            ])
 635
 636            if iJSON["figi"]:
 637                self.figi = iJSON["figi"]
 638                iJSON = iJSON | self.RequestTradingStatus()
 639
 640                info.extend([
 641                    splitLine,
 642                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 643                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 644                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 645                ])
 646
 647            info.append(splitLine)
 648
 649            if "type" in iJSON.keys() and iJSON["type"]:
 650                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 651
 652                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 653                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 654
 655            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 656                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 657
 658            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 659                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 660
 661            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 662                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 663
 664            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 665                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 666
 667            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 668                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 669
 670            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 671                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 672
 673            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 674                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 675
 676            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 677                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 678
 679            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 680                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 681
 682            if "currency" in iJSON.keys():
 683                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 684
 685            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 686                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 687
 688            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 689                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 690
 691            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 692                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 693
 694            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 695                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 696
 697            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 698                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 699
 700            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 701                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 702
 703            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 704                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 705
 706            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 707                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 708
 709            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 710                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 711
 712            iExt = None
 713            if iJSON["type"] == "Bonds":
 714                info.extend([
 715                    splitLine,
 716                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 717                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 718                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 719                        iJSON["nominal"]["currency"],
 720                    )),
 721                ])
 722
 723                if "floatingCouponFlag" in iJSON.keys():
 724                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 725
 726                if "amortizationFlag" in iJSON.keys():
 727                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 728
 729                info.append(splitLine)
 730
 731                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 732                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 733
 734                if iJSON["figi"]:
 735                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 736
 737                    info.extend([
 738                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 739                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 740                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 741                    ])
 742
 743                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 744                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 745                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 746                        iJSON["aciValue"]["currency"]
 747                    )))
 748
 749            if "currentPrice" in iJSON.keys():
 750                info.append(splitLine)
 751
 752                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 753                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 754
 755                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 756                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 757                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 758                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 759                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 760
 761                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 762                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 763
 764                info.extend([
 765                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 766                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 767                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 768                    )),
 769                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 770                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 771                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 772                    )),
 773                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 774                        "{:.2f}%{}".format(
 775                            iJSON["currentPrice"]["changes"],
 776                            " ({}{:.2f} {})".format(
 777                                "+" if bondChangesDelta > 0 else "",
 778                                bondChangesDelta,
 779                                aciCurrency
 780                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 781                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 782                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 783                                currency
 784                            ),
 785                        )
 786                    ),
 787                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 788                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 789                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 790                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 791                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 792                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 793                    )),
 794                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 795                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 796                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 797                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 798                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 799                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 800                    )),
 801                ])
 802
 803            if "lot" in iJSON.keys():
 804                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 805
 806            if "step" in iJSON.keys() and iJSON["step"] != 0:
 807                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 808
 809            # Add bond payment calendar:
 810            if iJSON["type"] == "Bonds":
 811                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 812                info.extend(["\n", strCalendar])
 813
 814            infoText += "".join(info)
 815
 816            if show:
 817                uLogger.info("{}".format(infoText))
 818
 819            else:
 820                uLogger.debug("{}".format(infoText))
 821
 822            if self.infoFile is not None:
 823                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 824                    fH.write(infoText)
 825
 826                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 827
 828        return infoText
 829
 830    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 831        """
 832        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 833
 834        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 835        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 836        :return: JSON formatted data with information about instrument.
 837        """
 838        tickerJSON = {}
 839        if self.moreDebug:
 840            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 841
 842        if not self.ticker:
 843            uLogger.warning("self.ticker variable is not be empty!")
 844
 845        else:
 846            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 847                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 848                raise Exception("Instrument not allowed")
 849
 850            if not self.iList:
 851                self.iList = self.Listing()
 852
 853            if self.ticker in self.iList["Shares"].keys():
 854                tickerJSON = self.iList["Shares"][self.ticker]
 855                if self.moreDebug:
 856                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 857
 858            elif self.ticker in self.iList["Currencies"].keys():
 859                tickerJSON = self.iList["Currencies"][self.ticker]
 860                if self.moreDebug:
 861                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 862
 863            elif self.ticker in self.iList["Bonds"].keys():
 864                tickerJSON = self.iList["Bonds"][self.ticker]
 865                if self.moreDebug:
 866                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 867
 868            elif self.ticker in self.iList["Etfs"].keys():
 869                tickerJSON = self.iList["Etfs"][self.ticker]
 870                if self.moreDebug:
 871                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 872
 873            elif self.ticker in self.iList["Futures"].keys():
 874                tickerJSON = self.iList["Futures"][self.ticker]
 875                if self.moreDebug:
 876                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 877
 878        if tickerJSON:
 879            self.figi = tickerJSON["figi"]
 880
 881            if requestPrice:
 882                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 883
 884                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 885                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 886
 887                else:
 888                    tickerJSON["currentPrice"]["changes"] = 0
 889
 890            if show:
 891                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 892
 893        else:
 894            if show:
 895                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 896
 897        return tickerJSON
 898
 899    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 900        """
 901        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 902
 903        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 904        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 905        :return: JSON formatted data with information about instrument.
 906        """
 907        figiJSON = {}
 908        if self.moreDebug:
 909            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 910
 911        if not self.figi:
 912            uLogger.warning("self.figi variable is not be empty!")
 913
 914        else:
 915            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 916                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 917                raise Exception("Instrument not allowed")
 918
 919            if not self.iList:
 920                self.iList = self.Listing()
 921
 922            for item in self.iList["Shares"].keys():
 923                if self.figi == self.iList["Shares"][item]["figi"]:
 924                    figiJSON = self.iList["Shares"][item]
 925
 926                    if self.moreDebug:
 927                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 928
 929                    break
 930
 931            if not figiJSON:
 932                for item in self.iList["Currencies"].keys():
 933                    if self.figi == self.iList["Currencies"][item]["figi"]:
 934                        figiJSON = self.iList["Currencies"][item]
 935
 936                        if self.moreDebug:
 937                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
 938
 939                        break
 940
 941            if not figiJSON:
 942                for item in self.iList["Bonds"].keys():
 943                    if self.figi == self.iList["Bonds"][item]["figi"]:
 944                        figiJSON = self.iList["Bonds"][item]
 945
 946                        if self.moreDebug:
 947                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
 948
 949                        break
 950
 951            if not figiJSON:
 952                for item in self.iList["Etfs"].keys():
 953                    if self.figi == self.iList["Etfs"][item]["figi"]:
 954                        figiJSON = self.iList["Etfs"][item]
 955
 956                        if self.moreDebug:
 957                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
 958
 959                        break
 960
 961            if not figiJSON:
 962                for item in self.iList["Futures"].keys():
 963                    if self.figi == self.iList["Futures"][item]["figi"]:
 964                        figiJSON = self.iList["Futures"][item]
 965
 966                        if self.moreDebug:
 967                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
 968
 969                        break
 970
 971        if figiJSON:
 972            self.figi = figiJSON["figi"]
 973            self.ticker = figiJSON["ticker"]
 974
 975            if requestPrice:
 976                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 977
 978                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
 979                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
 980
 981                else:
 982                    figiJSON["currentPrice"]["changes"] = 0
 983
 984            if show:
 985                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
 986
 987        else:
 988            if show:
 989                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
 990
 991        return figiJSON
 992
 993    def GetCurrentPrices(self, show: bool = True) -> dict:
 994        """
 995        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
 996        `{"buy": [{"price": 1243.8, "quantity": 193},
 997                  {"price": 1244.0, "quantity": 168},
 998                  {"price": 1244.8, "quantity": 5},
 999                  {"price": 1245.0, "quantity": 61},
1000                  {"price": 1245.4, "quantity": 60}],
1001          "sell": [{"price": 1243.6, "quantity": 8},
1002                   {"price": 1242.6, "quantity": 10},
1003                   {"price": 1242.4, "quantity": 18},
1004                   {"price": 1242.2, "quantity": 50},
1005                   {"price": 1242.0, "quantity": 113}],
1006          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1007        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1008        - sell: list of dicts with Buyers prices,
1009            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1010            - quantity: volume value by current price in lots,
1011        - limitUp: current trade session limit price, maximum,
1012        - limitDown: current trade session limit price, minimum,
1013        - lastPrice: last deal price of the instrument,
1014        - closePrice: previous trade session close price of the instrument.
1015
1016        See also: `SearchByTicker()` and `SearchByFIGI()`.
1017        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1018        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1019
1020        :param show: if `True` then print DOM to log and console.
1021        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1022                 If an error occurred then returns an empty record:
1023                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1024        """
1025        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1026
1027        if self.depth < 1:
1028            uLogger.error("Depth of Market (DOM) must be >=1!")
1029            raise Exception("Incorrect value")
1030
1031        if not (self.ticker or self.figi):
1032            uLogger.error("self.ticker or self.figi variables must be defined!")
1033            raise Exception("Ticker or FIGI required")
1034
1035        if self.ticker and not self.figi:
1036            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1037            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1038
1039        if not self.ticker and self.figi:
1040            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1041            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1042
1043        if not self.figi:
1044            uLogger.error("FIGI is not defined!")
1045            raise Exception("Ticker or FIGI required")
1046
1047        else:
1048            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1049
1050            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1051            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1052            self.body = str({"figi": self.figi, "depth": self.depth})
1053            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1054
1055            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1056                # list of dicts with sellers orders:
1057                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1058
1059                # list of dicts with buyers orders:
1060                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1061
1062                # max price of instrument at this time:
1063                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1064
1065                # min price of instrument at this time:
1066                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1067
1068                # last price of deal with instrument:
1069                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1070
1071                # last close price of instrument:
1072                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1073
1074            else:
1075                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1076                uLogger.debug("Server response: {}".format(pricesResponse))
1077
1078            if show:
1079                if prices["buy"] or prices["sell"]:
1080                    info = [
1081                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1082                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1083                            self.ticker,
1084                            self.figi,
1085                            self.depth,
1086                        ),
1087                        "-" * 60, "\n",
1088                        "             Orders of Buyers | Orders of Sellers\n",
1089                        "-" * 60, "\n",
1090                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1091                        "-" * 60, "\n",
1092                    ]
1093
1094                    if not prices["buy"]:
1095                        info.append("                              | No orders!\n")
1096                        sumBuy = 0
1097
1098                    else:
1099                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1100                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1101                        for item in maxMinSorted:
1102                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1103
1104                    if not prices["sell"]:
1105                        info.append("No orders!                    |\n")
1106                        sumSell = 0
1107
1108                    else:
1109                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1110                        for item in prices["sell"]:
1111                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1112
1113                    info.extend([
1114                        "-" * 60, "\n",
1115                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1116                        "-" * 60, "\n",
1117                    ])
1118
1119                    infoText = "".join(info)
1120
1121                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1122
1123                else:
1124                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1125
1126        return prices
1127
1128    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1129        """
1130        This method get and show information about all available broker instruments for current user account.
1131        If `instrumentsFile` string is not empty then also save information to this file.
1132
1133        :param show: if `True` then print results to console, if `False` — print only to file.
1134        :return: multi-lines string with all available broker instruments
1135        """
1136        if not self.iList:
1137            self.iList = self.Listing()
1138
1139        info = [
1140            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1141            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1142        ]
1143
1144        # add instruments count by type:
1145        for iType in self.iList.keys():
1146            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1147
1148        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1149        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1150
1151        # generating info tables with all instruments by type:
1152        for iType in self.iList.keys():
1153            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1154
1155            for instrument in self.iList[iType].keys():
1156                iName = self.iList[iType][instrument]["name"]  # instrument's name
1157                if len(iName) > 57:
1158                    iName = "{}...".format(iName[:54])  # right trim for a long string
1159
1160                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1161                    self.iList[iType][instrument]["ticker"],
1162                    iName,
1163                    self.iList[iType][instrument]["figi"],
1164                    self.iList[iType][instrument]["currency"],
1165                    self.iList[iType][instrument]["lot"],
1166                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1167                ))
1168
1169        infoText = "".join(info)
1170
1171        if show:
1172            uLogger.info(infoText)
1173
1174        if self.instrumentsFile:
1175            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1176                fH.write(infoText)
1177
1178            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1179
1180        return infoText
1181
1182    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1183        """
1184        This method search and show information about instruments by part of its ticker, FIGI or name.
1185        If `searchResultsFile` string is not empty then also save information to this file.
1186
1187        :param pattern: string with part of ticker, FIGI or instrument's name.
1188        :param show: if `True` then print results to console, if `False` — return list of result only.
1189        :return: list of dictionaries with all found instruments.
1190        """
1191        if not self.iList:
1192            self.iList = self.Listing()
1193
1194        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1195        compiledPattern = re.compile(pattern, re.IGNORECASE)
1196
1197        for iType in self.iList:
1198            for instrument in self.iList[iType].values():
1199                searchResult = compiledPattern.search(" ".join(
1200                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1201                ))
1202
1203                if searchResult:
1204                    searchResults[iType][instrument["ticker"]] = instrument
1205
1206        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1207        info = [
1208            "# Search results\n\n",
1209            "* **Search pattern:** [{}]\n".format(pattern),
1210            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1211            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1212        ]
1213        infoShort = info[:]
1214
1215        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1216        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1217        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1218
1219        if resultsLen == 0:
1220            info.append("\nNo results\n")
1221            infoShort.append("\nNo results\n")
1222            uLogger.warning("No results. Try changing your search pattern.")
1223
1224        else:
1225            for iType in searchResults:
1226                iTypeValuesCount = len(searchResults[iType].values())
1227                if iTypeValuesCount > 0:
1228                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1229                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1230
1231                    for instrument in searchResults[iType].values():
1232                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1233                            instrument["type"],
1234                            instrument["ticker"],
1235                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1236                            instrument["figi"],
1237                        ))
1238
1239                    if iTypeValuesCount <= 5:
1240                        infoShort.extend(info[-iTypeValuesCount:])
1241
1242                    else:
1243                        infoShort.extend(info[-5:])
1244                        infoShort.append(skippedLine)
1245
1246        infoText = "".join(info)
1247        infoTextShort = "".join(infoShort)
1248
1249        if show:
1250            uLogger.info(infoTextShort)
1251            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1252
1253        if self.searchResultsFile:
1254            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1255                fH.write(infoText)
1256
1257            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1258
1259        return searchResults
1260
1261    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1262        """
1263        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1264
1265        :param instruments: list of strings with tickers or FIGIs.
1266        :return: list with unique instrument FIGIs only.
1267        """
1268        requestedInstruments = []
1269        for iName in instruments:
1270            if iName not in self.aliases.keys():
1271                if iName not in requestedInstruments:
1272                    requestedInstruments.append(iName)
1273
1274            else:
1275                if iName not in requestedInstruments:
1276                    if self.aliases[iName] not in requestedInstruments:
1277                        requestedInstruments.append(self.aliases[iName])
1278
1279        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1280
1281        onlyUniqueFIGIs = []
1282        for iName in requestedInstruments:
1283            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1284                continue
1285
1286            self.ticker = iName
1287            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1288
1289            if not iData:
1290                self.ticker = ""
1291                self.figi = iName
1292
1293                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1294
1295                if not iData:
1296                    self.figi = ""
1297                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1298
1299            if iData and iData["figi"] not in onlyUniqueFIGIs:
1300                onlyUniqueFIGIs.append(iData["figi"])
1301
1302        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1303
1304        return onlyUniqueFIGIs
1305
1306    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1307        """
1308        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1309
1310        See limits: https://tinkoff.github.io/investAPI/limits/
1311
1312        If `pricesFile` string is not empty then also save information to this file.
1313
1314        :param instruments: list of strings with tickers or FIGIs.
1315        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1316        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1317                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1318        """
1319        if instruments is None or not instruments:
1320            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1321            raise Exception("Ticker or FIGI required")
1322
1323        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1324
1325        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1326
1327        iList = []  # trying to get info and current prices about all unique instruments:
1328        for self.figi in onlyUniqueFIGIs:
1329            iData = self.SearchByFIGI(requestPrice=True)
1330            iList.append(iData)
1331
1332        self.ShowListOfPrices(iList, show)
1333
1334        return iList
1335
1336    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1337        """
1338        Show table contains current prices of given instruments.
1339
1340        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1341                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1342        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1343        :return: multilines text in Markdown format as a table contains current prices.
1344        """
1345        infoText = ""
1346
1347        if show or self.pricesFile:
1348            info = [
1349                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1350                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1351                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1352            ]
1353
1354            for item in iList:
1355                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1356                    item["ticker"],
1357                    item["figi"],
1358                    item["type"],
1359                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1360                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1361                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1362                    "{} / {}".format(
1363                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1364                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1365                    ),
1366                    "{} / {}".format(
1367                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1368                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1369                    ),
1370                    item["currency"],
1371                ))
1372
1373            infoText = "".join(info)
1374
1375            if show:
1376                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1377
1378            if self.pricesFile:
1379                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1380                    fH.write(infoText)
1381
1382                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1383
1384        return infoText
1385
1386    def RequestTradingStatus(self) -> dict:
1387        """
1388        Requesting trading status for the instrument defined by `figi` variable.
1389
1390        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1391
1392        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1393
1394        :return: dictionary with trading status attributes. Response example:
1395                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1396                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1397        """
1398        if self.figi is None or not self.figi:
1399            uLogger.error("Variable `figi` must be defined for using this method!")
1400            raise Exception("FIGI required")
1401
1402        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1403
1404        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1405        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1406        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1407
1408        if self.moreDebug:
1409            uLogger.debug("Records about current trading status successfully received")
1410
1411        return tradingStatus
1412
1413    def RequestPortfolio(self) -> dict:
1414        """
1415        Requesting actual user's portfolio for current `accountId`.
1416
1417        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1418
1419        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1420
1421        :return: dictionary with user's portfolio.
1422        """
1423        if self.accountId is None or not self.accountId:
1424            uLogger.error("Variable `accountId` must be defined for using this method!")
1425            raise Exception("Account ID required")
1426
1427        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1428
1429        self.body = str({"accountId": self.accountId})
1430        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1431        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1432
1433        if self.moreDebug:
1434            uLogger.debug("Records about user's portfolio successfully received")
1435
1436        return rawPortfolio
1437
1438    def RequestPositions(self) -> dict:
1439        """
1440        Requesting open positions by currencies and instruments for current `accountId`.
1441
1442        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1443
1444        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1445
1446        :return: dictionary with open positions by instruments.
1447        """
1448        if self.accountId is None or not self.accountId:
1449            uLogger.error("Variable `accountId` must be defined for using this method!")
1450            raise Exception("Account ID required")
1451
1452        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1453
1454        self.body = str({"accountId": self.accountId})
1455        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1456        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1457
1458        if self.moreDebug:
1459            uLogger.debug("Records about current open positions successfully received")
1460
1461        return rawPositions
1462
1463    def RequestPendingOrders(self) -> list:
1464        """
1465        Requesting current actual pending orders for current `accountId`.
1466
1467        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1468
1469        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1470
1471        :return: list of dictionaries with pending orders.
1472        """
1473        if self.accountId is None or not self.accountId:
1474            uLogger.error("Variable `accountId` must be defined for using this method!")
1475            raise Exception("Account ID required")
1476
1477        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1478
1479        self.body = str({"accountId": self.accountId})
1480        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1481        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1482
1483        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1484
1485        return rawOrders
1486
1487    def RequestStopOrders(self) -> list:
1488        """
1489        Requesting current actual stop orders for current `accountId`.
1490
1491        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1492
1493        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1494
1495        :return: list of dictionaries with stop orders.
1496        """
1497        if self.accountId is None or not self.accountId:
1498            uLogger.error("Variable `accountId` must be defined for using this method!")
1499            raise Exception("Account ID required")
1500
1501        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1502
1503        self.body = str({"accountId": self.accountId})
1504        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1505        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1506
1507        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1508
1509        return rawStopOrders
1510
1511    def Overview(self, show: bool = False, details: str = "full") -> dict:
1512        """
1513        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1514        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1515        and `overviewBondsCalendarFile` are defined then also save information to file.
1516
1517        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1518        many requests about the state of the portfolio, and then, based on the received data, a large number
1519        of calculation and statistics are collected.
1520
1521        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1522        :param details: how detailed should the information be?
1523        - `full` — shows full available information about portfolio status (by default),
1524        - `positions` — shows only open positions,
1525        - `orders` — shows only sections of open limits and stop orders.
1526        - `digest` — show a short digest of the portfolio status,
1527        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1528        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1529        :return: dictionary with client's raw portfolio and some statistics.
1530        """
1531        if self.accountId is None or not self.accountId:
1532            uLogger.error("Variable `accountId` must be defined for using this method!")
1533            raise Exception("Account ID required")
1534
1535        view = {
1536            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1537                "headers": {},  # list of dictionaries, response headers without "positions" section
1538                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1539                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1540                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1541                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1542                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1543                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1544                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1545                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1546                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1547            },
1548            "stat": {  # --- some statistics calculated using "raw" sections:
1549                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1550                "availableRUB": 0.,  # available rubles (without other currencies)
1551                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1552                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1553                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1554                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1555                "sharesCostRUB": 0.,  # costs of all shares in RUB
1556                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1557                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1558                "futuresCostRUB": 0.,  # costs of all futures in RUB
1559                "Currencies": [],  # list of dictionaries of all currencies statistics
1560                "Shares": [],  # list of dictionaries of all shares statistics
1561                "Bonds": [],  # list of dictionaries of all bonds statistics
1562                "Etfs": [],  # list of dictionaries of all etfs statistics
1563                "Futures": [],  # list of dictionaries of all futures statistics
1564                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1565                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1566                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1567                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1568                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1569            },
1570            "analytics": {  # --- some analytics of portfolio:
1571                "distrByAssets": {},  # portfolio distribution by assets
1572                "distrByCompanies": {},  # portfolio distribution by companies
1573                "distrBySectors": {},  # portfolio distribution by sectors
1574                "distrByCurrencies": {},  # portfolio distribution by currencies
1575                "distrByCountries": {},  # portfolio distribution by countries
1576                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1577            }
1578        }
1579
1580        details = details.lower()
1581        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1582        if details not in availableDetails:
1583            details = "full"
1584            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1585
1586        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1587
1588        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1589        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1590        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1591        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1592
1593        # save response headers without "positions" section:
1594        for key in portfolioResponse.keys():
1595            if key != "positions":
1596                view["raw"]["headers"][key] = portfolioResponse[key]
1597
1598            else:
1599                continue
1600
1601        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1602        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1603        for item in portfolioResponse["positions"]:
1604            if item["instrumentType"] == "currency":
1605                self.figi = item["figi"]
1606                curr = self.SearchByFIGI(requestPrice=False)
1607
1608                # current price of currency in RUB:
1609                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1610                    "name": curr["name"],
1611                    "currentPrice": NanoToFloat(
1612                        item["currentPrice"]["units"],
1613                        item["currentPrice"]["nano"]
1614                    ),
1615                }
1616
1617                view["raw"]["Currencies"].append(item)
1618
1619            elif item["instrumentType"] == "share":
1620                view["raw"]["Shares"].append(item)
1621
1622            elif item["instrumentType"] == "bond":
1623                view["raw"]["Bonds"].append(item)
1624
1625            elif item["instrumentType"] == "etf":
1626                view["raw"]["Etfs"].append(item)
1627
1628            elif item["instrumentType"] == "futures":
1629                view["raw"]["Futures"].append(item)
1630
1631            else:
1632                continue
1633
1634        # how many volume of currencies (by ISO currency name) are blocked:
1635        for item in view["raw"]["positions"]["blocked"]:
1636            blocked = NanoToFloat(item["units"], item["nano"])
1637            if blocked > 0:
1638                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1639
1640        # how many volume of instruments (by FIGI) are blocked:
1641        for item in view["raw"]["positions"]["securities"]:
1642            blocked = int(item["blocked"])
1643            if blocked > 0:
1644                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1645
1646        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1647
1648        if "rub" in allBlocked.keys():
1649            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1650
1651        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1652        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1653        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1654        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1655        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1656        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1657        view["stat"]["portfolioCostRUB"] = sum([
1658            view["stat"]["allCurrenciesCostRUB"],
1659            view["stat"]["sharesCostRUB"],
1660            view["stat"]["bondsCostRUB"],
1661            view["stat"]["etfsCostRUB"],
1662            view["stat"]["futuresCostRUB"],
1663        ])
1664
1665        # --- calculating some portfolio statistics:
1666        byComp = {}  # distribution by companies
1667        bySect = {}  # distribution by sectors
1668        byCurr = {}  # distribution by currencies (include RUB)
1669        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1670        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1671
1672        for item in portfolioResponse["positions"]:
1673            self.figi = item["figi"]
1674            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1675
1676            if instrument:
1677                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1678                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1679
1680                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1681                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1682
1683                else:
1684                    blocked = 0
1685
1686                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1687                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1688                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1689                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1690                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1691                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1692                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1693                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1694                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1695                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1696                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1697                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1698
1699                statData = {
1700                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1701                    "ticker": instrument["ticker"],  # ticker by FIGI
1702                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1703                    "volume": volume,  # available volume of instrument
1704                    "lots": lots,  # volume in lots of instrument
1705                    "direction": direction,  # direction of an instrument's position: short or long
1706                    "blocked": blocked,  # blocked volume of currency or instrument
1707                    "currentPrice": curPrice,  # current instrument's price in basic asset
1708                    "average": average,  # current average position price
1709                    "cost": cost,  # current cost of all volume of instrument in basic asset
1710                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1711                    "costRUB": costRUB,  # cost of instrument in ruble
1712                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1713                    "profit": profit,  # expected profit at current moment
1714                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1715                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1716                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1717                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1718                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1719                    "step": instrument["step"],  # minimum price increment
1720                }
1721
1722                # adding distribution by unique countries:
1723                if statData["country"] not in byCountry.keys():
1724                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1725
1726                else:
1727                    byCountry[statData["country"]]["cost"] += costRUB
1728                    byCountry[statData["country"]]["percent"] += percentCostRUB
1729
1730                if item["instrumentType"] != "currency":
1731                    # adding distribution by unique companies:
1732                    if statData["name"]:
1733                        if statData["name"] not in byComp.keys():
1734                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1735
1736                        else:
1737                            byComp[statData["name"]]["cost"] += costRUB
1738                            byComp[statData["name"]]["percent"] += percentCostRUB
1739
1740                    # adding distribution by unique sectors:
1741                    if statData["sector"] not in bySect.keys():
1742                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1743
1744                    else:
1745                        bySect[statData["sector"]]["cost"] += costRUB
1746                        bySect[statData["sector"]]["percent"] += percentCostRUB
1747
1748                # adding distribution by unique currencies:
1749                if currency not in byCurr.keys():
1750                    byCurr[currency] = {
1751                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1752                        "cost": costRUB,
1753                        "percent": percentCostRUB
1754                    }
1755
1756                else:
1757                    byCurr[currency]["cost"] += costRUB
1758                    byCurr[currency]["percent"] += percentCostRUB
1759
1760                # saving statistics for every instrument:
1761                if item["instrumentType"] == "currency":
1762                    view["stat"]["Currencies"].append(statData)
1763
1764                    # update dict with free funds for trading (total - blocked) by currencies
1765                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1766                    view["stat"]["funds"][currency] = {
1767                        "total": volume,
1768                        "totalCostRUB": costRUB,  # total volume cost in rubles
1769                        "free": volume - blocked,
1770                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1771                    }
1772
1773                elif item["instrumentType"] == "share":
1774                    view["stat"]["Shares"].append(statData)
1775
1776                elif item["instrumentType"] == "bond":
1777                    view["stat"]["Bonds"].append(statData)
1778
1779                elif item["instrumentType"] == "etf":
1780                    view["stat"]["Etfs"].append(statData)
1781
1782                elif item["instrumentType"] == "Futures":
1783                    view["stat"]["Futures"].append(statData)
1784
1785                else:
1786                    continue
1787
1788        # total changes in Russian Ruble:
1789        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1790        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1791        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1792        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1793        view["stat"]["funds"]["rub"] = {
1794            "total": view["stat"]["availableRUB"],
1795            "totalCostRUB": view["stat"]["availableRUB"],
1796            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1797            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1798        }
1799
1800        # --- pending orders sector data:
1801        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1802        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1803
1804        for item in view["raw"]["orders"]:
1805            self.figi = item["figi"]
1806
1807            if item["figi"] not in uniquePendingOrdersFIGIs:
1808                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1809
1810                uniquePendingOrdersFIGIs.append(item["figi"])
1811                uniquePendingOrders[item["figi"]] = instrument
1812
1813            else:
1814                instrument = uniquePendingOrders[item["figi"]]
1815
1816            if instrument:
1817                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1818                orderType = TKS_ORDER_TYPES[item["orderType"]]
1819                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1820                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1821
1822                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1823                if item["direction"] == "ORDER_DIRECTION_BUY":
1824                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1825
1826                else:
1827                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1828
1829                # requested price for order execution:
1830                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1831
1832                # necessary changes in percent to reach target from current price:
1833                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1834
1835                view["stat"]["orders"].append({
1836                    "orderID": item["orderId"],  # orderId number parameter of current order
1837                    "figi": item["figi"],  # FIGI identification
1838                    "ticker": instrument["ticker"],  # ticker name by FIGI
1839                    "lotsRequested": item["lotsRequested"],  # requested lots value
1840                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1841                    "currentPrice": lastPrice,  # current instrument's price for defined action
1842                    "targetPrice": target,  # requested price for order execution in base currency
1843                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1844                    "percentChanges": changes,  # changes in percent to target from current price
1845                    "currency": item["currency"],  # instrument's currency name
1846                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1847                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1848                    "status": orderState,  # order status from TKS_ORDER_STATES
1849                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1850                })
1851
1852        # --- stop orders sector data:
1853        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1854        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1855
1856        for item in view["raw"]["stopOrders"]:
1857            self.figi = item["figi"]
1858
1859            if item["figi"] not in uniqueStopOrdersFIGIs:
1860                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1861
1862                uniqueStopOrdersFIGIs.append(item["figi"])
1863                uniqueStopOrders[item["figi"]] = instrument
1864
1865            else:
1866                instrument = uniqueStopOrders[item["figi"]]
1867
1868            if instrument:
1869                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1870                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1871                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1872
1873                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1874                if "expirationTime" in item.keys():
1875                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1876                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1877
1878                else:
1879                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1880                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1881
1882                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1883                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1884                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1885
1886                else:
1887                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1888
1889                # requested price when stop-order executed:
1890                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1891
1892                # price for limit-order, set up when stop-order executed:
1893                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1894
1895                # necessary changes in percent to reach target from current price:
1896                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1897
1898                view["stat"]["stopOrders"].append({
1899                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1900                    "figi": item["figi"],  # FIGI identification
1901                    "ticker": instrument["ticker"],  # ticker name by FIGI
1902                    "lotsRequested": item["lotsRequested"],  # requested lots value
1903                    "currentPrice": lastPrice,  # current instrument's price for defined action
1904                    "targetPrice": target,  # requested price for stop-order execution in base currency
1905                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1906                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1907                    "percentChanges": changes,  # changes in percent to target from current price
1908                    "currency": item["currency"],  # instrument's currency name
1909                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1910                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1911                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1912                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1913                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1914                })
1915
1916        # --- calculating data for analytics section:
1917        # portfolio distribution by assets:
1918        view["analytics"]["distrByAssets"] = {
1919            "Ruble": {
1920                "uniques": 1,
1921                "cost": view["stat"]["availableRUB"],
1922                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1923            },
1924            "Currencies": {
1925                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1926                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1927                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1928            },
1929            "Shares": {
1930                "uniques": len(view["stat"]["Shares"]),
1931                "cost": view["stat"]["sharesCostRUB"],
1932                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1933            },
1934            "Bonds": {
1935                "uniques": len(view["stat"]["Bonds"]),
1936                "cost": view["stat"]["bondsCostRUB"],
1937                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1938            },
1939            "Etfs": {
1940                "uniques": len(view["stat"]["Etfs"]),
1941                "cost": view["stat"]["etfsCostRUB"],
1942                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1943            },
1944            "Futures": {
1945                "uniques": len(view["stat"]["Futures"]),
1946                "cost": view["stat"]["futuresCostRUB"],
1947                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1948            },
1949        }
1950
1951        # portfolio distribution by companies:
1952        view["analytics"]["distrByCompanies"]["All money cash"] = {
1953            "ticker": "",
1954            "cost": view["stat"]["allCurrenciesCostRUB"],
1955            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1956        }
1957        view["analytics"]["distrByCompanies"].update(byComp)
1958
1959        # portfolio distribution by sectors:
1960        view["analytics"]["distrBySectors"]["All money cash"] = {
1961            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1962            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1963        }
1964        view["analytics"]["distrBySectors"].update(bySect)
1965
1966        # portfolio distribution by currencies:
1967        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1968            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1969
1970            if self.moreDebug:
1971                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1972
1973        view["analytics"]["distrByCurrencies"].update(byCurr)
1974        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1975        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1976
1977        # portfolio distribution by countries:
1978        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1979            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1980
1981            if self.moreDebug:
1982                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1983
1984        view["analytics"]["distrByCountries"].update(byCountry)
1985        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1986        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1987
1988        # --- Prepare text statistics overview in human-readable:
1989        if show:
1990            # Whatever the value `details`, header not changes:
1991            info = [
1992                "# Client's portfolio\n\n",
1993                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1994                "* **Account ID:** [{}]\n".format(self.accountId),
1995            ]
1996
1997            if details in ["full", "positions", "digest"]:
1998                info.extend([
1999                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2000                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2001                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2002                        view["stat"]["totalChangesRUB"],
2003                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2004                        view["stat"]["totalChangesPercentRUB"],
2005                    ),
2006                ])
2007
2008            if details in ["full", "positions"]:
2009                info.extend([
2010                    "## Open positions\n\n",
2011                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2012                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2013                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2014                        "{:.2f} ({:.2f}) rub".format(
2015                            view["stat"]["availableRUB"],
2016                            view["stat"]["blockedRUB"],
2017                        )
2018                    )
2019                ])
2020
2021                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2022                    return [
2023                        "|                             |                                 |          |              |              |                     |                              |\n",
2024                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2025                            noTradeStr if noTradeStr else typeStr,
2026                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2027                        ),
2028                    ]
2029
2030                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2031                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2032                        "{} [{}]".format(data["ticker"], data["figi"]),
2033                        "{:.2f} ({:.2f}) {}".format(
2034                            data["volume"],
2035                            data["blocked"],
2036                            data["currency"],
2037                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2038                            data["volume"],
2039                            data["blocked"],
2040                        ),
2041                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2042                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2043                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2044                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2045                        "{}{:.2f} {} ({}{:.2f}%)".format(
2046                            "+" if data["profit"] > 0 else "",
2047                            data["profit"], data["baseCurrencyName"],
2048                            "+" if data["percentProfit"] > 0 else "",
2049                            data["percentProfit"],
2050                        ),
2051                    )
2052
2053                # --- Show currencies section:
2054                if view["stat"]["Currencies"]:
2055                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2056                    for item in view["stat"]["Currencies"]:
2057                        info.append(_InfoStr(item, showCurrencyName=True))
2058
2059                else:
2060                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2061
2062                # --- Show shares section:
2063                if view["stat"]["Shares"]:
2064                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2065
2066                    for item in view["stat"]["Shares"]:
2067                        info.append(_InfoStr(item))
2068
2069                else:
2070                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2071
2072                # --- Show bonds section:
2073                if view["stat"]["Bonds"]:
2074                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2075
2076                    for item in view["stat"]["Bonds"]:
2077                        info.append(_InfoStr(item))
2078
2079                else:
2080                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2081
2082                # --- Show etfs section:
2083                if view["stat"]["Etfs"]:
2084                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2085
2086                    for item in view["stat"]["Etfs"]:
2087                        info.append(_InfoStr(item))
2088
2089                else:
2090                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2091
2092                # --- Show futures section:
2093                if view["stat"]["Futures"]:
2094                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2095
2096                    for item in view["stat"]["Futures"]:
2097                        info.append(_InfoStr(item))
2098
2099                else:
2100                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2101
2102            if details in ["full", "orders"]:
2103                # --- Show pending orders section:
2104                if view["stat"]["orders"]:
2105                    info.extend([
2106                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2107                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2108                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2109                    ])
2110
2111                    for item in view["stat"]["orders"]:
2112                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2113                            "{} [{}]".format(item["ticker"], item["figi"]),
2114                            item["orderID"],
2115                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2116                            "{} {} ({}{:.2f}%)".format(
2117                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2118                                item["baseCurrencyName"],
2119                                "+" if item["percentChanges"] > 0 else "",
2120                                float(item["percentChanges"]),
2121                            ),
2122                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2123                            item["action"],
2124                            item["type"],
2125                            item["date"],
2126                        ))
2127
2128                else:
2129                    info.append("\n## Total pending limit-orders: 0\n")
2130
2131                # --- Show stop orders section:
2132                if view["stat"]["stopOrders"]:
2133                    info.extend([
2134                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2135                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2136                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2137                    ])
2138
2139                    for item in view["stat"]["stopOrders"]:
2140                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2141                            "{} [{}]".format(item["ticker"], item["figi"]),
2142                            item["orderID"],
2143                            item["lotsRequested"],
2144                            "{} {} ({}{:.2f}%)".format(
2145                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2146                                item["baseCurrencyName"],
2147                                "+" if item["percentChanges"] > 0 else "",
2148                                float(item["percentChanges"]),
2149                            ),
2150                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2151                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2152                            item["action"],
2153                            item["type"],
2154                            item["expType"],
2155                            item["createDate"],
2156                            item["expDate"],
2157                        ))
2158
2159                else:
2160                    info.append("\n## Total stop-orders: 0\n")
2161
2162            if details in ["full", "analytics"]:
2163                # -- Show analytics section:
2164                if view["stat"]["portfolioCostRUB"] > 0:
2165                    info.extend([
2166                        "\n# Analytics\n"
2167                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2168                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2169                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2170                            view["stat"]["totalChangesRUB"],
2171                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2172                            view["stat"]["totalChangesPercentRUB"],
2173                        ),
2174                        "\n## Portfolio distribution by assets\n"
2175                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2176                        "|------------------------------------|---------|---------|--------------------|\n",
2177                    ])
2178
2179                    for key in view["analytics"]["distrByAssets"].keys():
2180                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2181                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2182                                key,
2183                                view["analytics"]["distrByAssets"][key]["uniques"],
2184                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2185                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2186                            ))
2187
2188                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2189
2190                    info.extend([
2191                        "\n## Portfolio distribution by companies\n"
2192                        "\n| Company                                      | Percent | Current cost       |\n",
2193                        aSepLine,
2194                    ])
2195
2196                    for company in view["analytics"]["distrByCompanies"].keys():
2197                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2198                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2199                                "{}{}".format(
2200                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2201                                    company,
2202                                ),
2203                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2204                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2205                            ))
2206
2207                    info.extend([
2208                        "\n## Portfolio distribution by sectors\n"
2209                        "\n| Sector                                       | Percent | Current cost       |\n",
2210                        aSepLine,
2211                    ])
2212
2213                    for sector in view["analytics"]["distrBySectors"].keys():
2214                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2215                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2216                                sector,
2217                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2218                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2219                            ))
2220
2221                    info.extend([
2222                        "\n## Portfolio distribution by currencies\n"
2223                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2224                        aSepLine,
2225                    ])
2226
2227                    for curr in view["analytics"]["distrByCurrencies"].keys():
2228                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2229                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2230                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2231                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2232                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2233                            ))
2234
2235                    info.extend([
2236                        "\n## Portfolio distribution by countries\n"
2237                        "\n| Assets by country                            | Percent | Current cost       |\n",
2238                        aSepLine,
2239                    ])
2240
2241                    for country in view["analytics"]["distrByCountries"].keys():
2242                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2243                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2244                                country,
2245                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2246                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2247                            ))
2248
2249            if details in ["full", "calendar"]:
2250                # -- Show bonds payment calendar section:
2251                if view["stat"]["Bonds"]:
2252                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2253                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2254                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2255
2256                else:
2257                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2258
2259            infoText = "".join(info)
2260
2261            uLogger.info(infoText)
2262
2263            if details == "full" and self.overviewFile:
2264                filename = self.overviewFile
2265
2266            elif details == "digest" and self.overviewDigestFile:
2267                filename = self.overviewDigestFile
2268
2269            elif details == "positions" and self.overviewPositionsFile:
2270                filename = self.overviewPositionsFile
2271
2272            elif details == "orders" and self.overviewOrdersFile:
2273                filename = self.overviewOrdersFile
2274
2275            elif details == "analytics" and self.overviewAnalyticsFile:
2276                filename = self.overviewAnalyticsFile
2277
2278            elif details == "calendar" and self.overviewBondsCalendarFile:
2279                filename = self.overviewBondsCalendarFile
2280
2281            else:
2282                filename = ""
2283
2284            if filename:
2285                with open(filename, "w", encoding="UTF-8") as fH:
2286                    fH.write(infoText)
2287
2288                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2289
2290        return view
2291
2292    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2293        """
2294        Returns history operations between two given dates for current `accountId`.
2295        If `reportFile` string is not empty then also save human-readable report.
2296        Shows some statistical data of closed positions.
2297
2298        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2299        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2300        :param show: if `True` then also prints all records to the console.
2301        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2302        :return: original list of dictionaries with history of deals records from API ("operations" key):
2303                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2304                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2305        """
2306        if self.accountId is None or not self.accountId:
2307            uLogger.error("Variable `accountId` must be defined for using this method!")
2308            raise Exception("Account ID required")
2309
2310        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2311
2312        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2313
2314        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2315        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2316        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2317        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2318        customStat = {}  # custom statistics in additional to responseJSON
2319
2320        # --- output report in human-readable format:
2321        if show or self.reportFile:
2322            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2323            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2324            nextDay = ""
2325
2326            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2327
2328            if len(ops) > 0:
2329                customStat = {
2330                    "opsCount": 0,  # total operations count
2331                    "buyCount": 0,  # buy operations
2332                    "sellCount": 0,  # sell operations
2333                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2334                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2335                    "payIn": {"rub": 0.},  # Deposit brokerage account
2336                    "payOut": {"rub": 0.},  # Withdrawals
2337                    "divs": {"rub": 0.},  # Dividends income
2338                    "coupons": {"rub": 0.},  # Coupon's income
2339                    "brokerCom": {"rub": 0.},  # Service commissions
2340                    "serviceCom": {"rub": 0.},  # Service commissions
2341                    "marginCom": {"rub": 0.},  # Margin commissions
2342                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2343                }
2344
2345                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2346                for item in ops:
2347                    if item["state"] == "OPERATION_STATE_EXECUTED":
2348                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2349
2350                        # count buy operations:
2351                        if "_BUY" in item["operationType"]:
2352                            customStat["buyCount"] += 1
2353
2354                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2355                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2356
2357                            else:
2358                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2359
2360                        # count sell operations:
2361                        elif "_SELL" in item["operationType"]:
2362                            customStat["sellCount"] += 1
2363
2364                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2365                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2366
2367                            else:
2368                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2369
2370                        # count incoming operations:
2371                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2372                            if item["payment"]["currency"] in customStat["payIn"].keys():
2373                                customStat["payIn"][item["payment"]["currency"]] += payment
2374
2375                            else:
2376                                customStat["payIn"][item["payment"]["currency"]] = payment
2377
2378                        # count withdrawals operations:
2379                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2380                            if item["payment"]["currency"] in customStat["payOut"].keys():
2381                                customStat["payOut"][item["payment"]["currency"]] += payment
2382
2383                            else:
2384                                customStat["payOut"][item["payment"]["currency"]] = payment
2385
2386                        # count dividends income:
2387                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2388                            if item["payment"]["currency"] in customStat["divs"].keys():
2389                                customStat["divs"][item["payment"]["currency"]] += payment
2390
2391                            else:
2392                                customStat["divs"][item["payment"]["currency"]] = payment
2393
2394                        # count coupon's income:
2395                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2396                            if item["payment"]["currency"] in customStat["coupons"].keys():
2397                                customStat["coupons"][item["payment"]["currency"]] += payment
2398
2399                            else:
2400                                customStat["coupons"][item["payment"]["currency"]] = payment
2401
2402                        # count broker commissions:
2403                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2404                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2405                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2406
2407                            else:
2408                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2409
2410                        # count service commissions:
2411                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2412                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2413                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2414
2415                            else:
2416                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2417
2418                        # count margin commissions:
2419                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2420                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2421                                customStat["marginCom"][item["payment"]["currency"]] += payment
2422
2423                            else:
2424                                customStat["marginCom"][item["payment"]["currency"]] = payment
2425
2426                        # count withholding taxes:
2427                        elif "_TAX" in item["operationType"]:
2428                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2429                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2430
2431                            else:
2432                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2433
2434                        else:
2435                            continue
2436
2437                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2438
2439                # --- view "Actions" lines:
2440                info.extend([
2441                    "| Report sections            |                               |                              |                      |                        |\n",
2442                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2443                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2444                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2445                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2446                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2447                    ),
2448                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2449                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2450                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2451                    ),
2452                ])
2453
2454                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2455                for key in opsKeys:
2456                    if key == "rub":
2457                        continue
2458
2459                    info.extend([
2460                        "|                            |                               | {:<28} |                      |                        |\n".format(
2461                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2462                        ),
2463                        "|                            |                               | {:<28} |                      |                        |\n".format(
2464                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2465                        ),
2466                    ])
2467
2468                info.append(splitLine1)
2469
2470                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2471                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2472                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2473                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2474                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2475                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2476                    )
2477
2478                # --- view "Payments" lines:
2479                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2480                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2481
2482                for key in paymentsKeys:
2483                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2484
2485                info.append(splitLine1)
2486
2487                # --- view "Commissions and taxes" lines:
2488                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2489                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2490
2491                for key in comKeys:
2492                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2493
2494                info.append(splitLine1)
2495
2496                info.extend([
2497                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2498                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2499                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2500                ])
2501
2502            else:
2503                info.append("Broker returned no operations during this period\n")
2504
2505            # --- view "Operations" section:
2506            for item in ops:
2507                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2508                    continue
2509
2510                else:
2511                    self.figi = item["figi"] if item["figi"] else ""
2512                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2513                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2514
2515                    # group of deals during one day:
2516                    if nextDay and item["date"].split("T")[0] != nextDay:
2517                        info.append(splitLine2)
2518                        nextDay = ""
2519
2520                    else:
2521                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2522
2523                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2524                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2525                        self.figi if self.figi else "—",
2526                        instrument["ticker"] if instrument else "—",
2527                        instrument["type"] if instrument else "—",
2528                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2529                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2530                        TKS_OPERATION_STATES[item["state"]],
2531                        TKS_OPERATION_TYPES[item["operationType"]],
2532                    ))
2533
2534            infoText = "".join(info)
2535
2536            if show:
2537                if self.moreDebug:
2538                    uLogger.debug("Records about history of a client's operations successfully received")
2539
2540                uLogger.info(infoText)
2541
2542            if self.reportFile:
2543                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2544                    fH.write(infoText)
2545
2546                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2547
2548        return ops, customStat
2549
2550    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2551        """
2552        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2553
2554        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2555        Warning! Broker server used ISO UTC time by default.
2556
2557        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2558        Also, `historyFile` used to update history with `onlyMissing` parameter.
2559
2560        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2561
2562        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2563        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2564        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2565                         `"hour"`, `"day"`. Default: `"hour"`.
2566        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2567                            False by default. Warning! History appends only from last candle to current time
2568                            with always update last candle!
2569        :param csvSep: separator if csv-file is used, `,` by default.
2570        :param show: if `True` then also prints Pandas DataFrame to the console.
2571        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2572                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2573        """
2574        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2575        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2576        history = None  # empty pandas object for history
2577
2578        if interval not in TKS_CANDLE_INTERVALS.keys():
2579            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2580            raise Exception("Incorrect value")
2581
2582        if not (self.ticker or self.figi):
2583            uLogger.error("Ticker or FIGI must be defined!")
2584            raise Exception("Ticker or FIGI required")
2585
2586        if self.ticker and not self.figi:
2587            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2588            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2589
2590        if self.figi and not self.ticker:
2591            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2592            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2593
2594        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2595        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2596        if interval.lower() != "day":
2597            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2598
2599        delta = dtEnd - dtStart  # current UTC time minus last time in file
2600        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2601
2602        # calculate history length in candles:
2603        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2604        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2605            length += 1  # to avoid fraction time
2606
2607        # calculate data blocks count:
2608        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2609
2610        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2611        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2612        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2613        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2614        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2615
2616        tempOld = None  # pandas object for old history, if --only-missing key present
2617        lastTime = None  # datetime object of last old candle in file
2618
2619        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2620            uLogger.debug("--only-missing key present, add only last missing candles...")
2621            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2622
2623            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2624
2625            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2626            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2627            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2628            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2629
2630            # get last datetime object from last string in file or minus 1 delta if file is empty:
2631            if len(tempOld) > 0:
2632                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2633
2634            else:
2635                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2636
2637            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2638
2639        responseJSONs = []  # raw history blocks of data
2640
2641        blockEnd = dtEnd
2642        for item in range(blocks):
2643            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2644            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2645
2646            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2647                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2648            ))
2649
2650            if blockStart == blockEnd:
2651                uLogger.debug("Skipped this zero-length block...")
2652
2653            else:
2654                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2655                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2656                self.body = str({
2657                    "figi": self.figi,
2658                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2659                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2660                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2661                })
2662                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2663
2664                if "code" in responseJSON.keys():
2665                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2666
2667                else:
2668                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2669                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2670
2671                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2672
2673            blockEnd = blockStart
2674
2675        printCount = len(responseJSONs)  # candles to show in console
2676        if responseJSONs:
2677            tempHistory = pd.DataFrame(
2678                data={
2679                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2680                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2681                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2682                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2683                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2684                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2685                    "volume": [int(item["volume"]) for item in responseJSONs],
2686                },
2687                index=range(len(responseJSONs)),
2688                columns=["date", "time", "open", "high", "low", "close", "volume"],
2689            )
2690            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2691            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2692
2693            # append only newest candles to old history if --only-missing key present:
2694            if onlyMissing and tempOld is not None and lastTime is not None:
2695                index = 0  # find start index in tempHistory data:
2696
2697                for i, item in tempHistory.iterrows():
2698                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2699
2700                    if curTime == lastTime:
2701                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2702                        index = i
2703                        printCount = index + 1
2704                        break
2705
2706                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2707
2708            else:
2709                history = tempHistory  # if no `--only-missing` key then load full data from server
2710
2711            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2712
2713        if history is not None and not history.empty:
2714            if show:
2715                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2716                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2717                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2718                ))
2719
2720        else:
2721            uLogger.warning("Received an empty candles history!")
2722
2723        if self.historyFile is not None:
2724            if history is not None and not history.empty:
2725                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2726                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2727
2728            else:
2729                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2730
2731        else:
2732            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2733
2734        return history
2735
2736    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2737        """
2738        Load candles history from csv-file and return Pandas DataFrame object.
2739
2740        See also: `History()` and `ShowHistoryChart()` methods.
2741
2742        :param filePath: path to csv-file to open.
2743        """
2744        loadedHistory = None  # init candles data object
2745
2746        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2747
2748        if os.path.exists(filePath):
2749            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2750
2751            tfStr = self.priceModel.FormattedDelta(
2752                self.priceModel.timeframe,
2753                "{days} days {hours}h {minutes}m {seconds}s",
2754            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2755                self.priceModel.timeframe,
2756                "{hours}h {minutes}m {seconds}s",
2757            )
2758
2759            if loadedHistory is not None and not loadedHistory.empty:
2760                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2761                    len(loadedHistory),
2762                    tfStr,
2763                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2764                )
2765
2766            else:
2767                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2768
2769        else:
2770            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2771
2772        return loadedHistory
2773
2774    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2775        """
2776        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2777
2778        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2779        Default: `index.html` (both for interact and non-interact candlesticks chart).
2780
2781        See also: `History()` and `LoadHistory()` methods.
2782
2783        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2784        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2785                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2786                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2787                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2788        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2789                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2790        """
2791        if isinstance(candles, str):
2792            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2793            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2794
2795        elif isinstance(candles, pd.DataFrame):
2796            self.priceModel.prices = candles  # set candles chain from variable
2797            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2798
2799            if "datetime" not in candles.columns:
2800                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2801
2802        else:
2803            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2804            raise Exception("Incorrect value")
2805
2806        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2807
2808        if interact:
2809            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2810
2811            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2812
2813        else:
2814            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2815
2816            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2817
2818        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2819
2820    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2821        """
2822        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2823        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2824
2825        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2826
2827        :param operation: string "Buy" or "Sell".
2828        :param lots: volume, integer count of lots >= 1.
2829        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2830        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2831        :param expDate: string "Undefined" by default or local date in future,
2832                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2833        :return: JSON with response from broker server.
2834        """
2835        if self.accountId is None or not self.accountId:
2836            uLogger.error("Variable `accountId` must be defined for using this method!")
2837            raise Exception("Account ID required")
2838
2839        if operation is None or not operation or operation not in ("Buy", "Sell"):
2840            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2841            raise Exception("Incorrect value")
2842
2843        if lots is None or lots < 1:
2844            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2845            lots = 1
2846
2847        if tp is None or tp < 0:
2848            tp = 0
2849
2850        if sl is None or sl < 0:
2851            sl = 0
2852
2853        if expDate is None or not expDate:
2854            expDate = "Undefined"
2855
2856        if not (self.ticker or self.figi):
2857            uLogger.error("Ticker or FIGI must be defined!")
2858            raise Exception("Ticker or FIGI required")
2859
2860        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2861        self.ticker = instrument["ticker"]
2862        self.figi = instrument["figi"]
2863
2864        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2865
2866        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2867        self.body = str({
2868            "figi": self.figi,
2869            "quantity": str(lots),
2870            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2871            "accountId": str(self.accountId),
2872            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2873        })
2874        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2875
2876        if "orderId" in response.keys():
2877            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2878                operation, response["orderId"],
2879                self.ticker, self.figi, lots,
2880                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2881                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2882                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2883            ))
2884
2885            if tp > 0:
2886                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2887
2888            if sl > 0:
2889                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2890
2891        else:
2892            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2893
2894        return response
2895
2896    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2897        """
2898        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2899        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2900
2901        See also: `Order()` and `Trade()` docstrings.
2902
2903        :param lots: volume, integer count of lots >= 1.
2904        :param tp: float > 0, take profit price of stop-order.
2905        :param sl: float > 0, stop loss price of stop-order.
2906        :param expDate: it's a local date in future.
2907                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2908        :return: JSON with response from broker server.
2909        """
2910        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2911
2912    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2913        """
2914        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2915        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2916
2917        See also: `Order()` and `Trade()` docstrings.
2918
2919        :param lots: volume, integer count of lots >= 1.
2920        :param tp: float > 0, take profit price of stop-order.
2921        :param sl: float > 0, stop loss price of stop-order.
2922        :param expDate: it's a local date in the future.
2923                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2924        :return: JSON with response from broker server.
2925        """
2926        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2927
2928    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2929        """
2930        Close position of given instruments.
2931
2932        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2933        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2934                         This avoids unnecessary downloading data from the server.
2935        """
2936        if instruments is None or not instruments:
2937            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2938            raise Exception("Ticker or FIGI required")
2939
2940        if isinstance(instruments, str):
2941            instruments = [instruments]
2942
2943        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2944        if uniqueInstruments:
2945            if portfolio is None or not portfolio:
2946                portfolio = self.Overview(show=False)
2947
2948            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2949            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2950
2951            for self.figi in uniqueInstruments:
2952                if self.figi not in allOpened:
2953                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2954                    continue
2955
2956                # search open trade info about instrument by ticker:
2957                instrument = {}
2958                for iType in TKS_INSTRUMENTS:
2959                    if instrument:
2960                        break
2961
2962                    for item in portfolio["stat"][iType]:
2963                        if item["figi"] == self.figi:
2964                            instrument = item
2965                            break
2966
2967                if instrument:
2968                    self.ticker = instrument["ticker"]
2969                    self.figi = instrument["figi"]
2970
2971                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2972                        self.ticker,
2973                        self.figi,
2974                        int(instrument["volume"]),
2975                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2976                    ))
2977
2978                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2979
2980                    if tradeLots > 0:
2981                        if instrument["blocked"] > 0:
2982                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2983                                instrument["blocked"],
2984                                self.ticker,
2985                                tradeLots,
2986                            ))
2987
2988                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2989                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
2990
2991                    else:
2992                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
2993
2994    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2995        """
2996        Close all positions of given instruments with defined type.
2997
2998        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
2999        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3000                         This avoids unnecessary downloading data from the server.
3001        """
3002        if iType not in TKS_INSTRUMENTS:
3003            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3004
3005        else:
3006            if portfolio is None or not portfolio:
3007                portfolio = self.Overview(show=False)
3008
3009            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3010            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3011
3012            if tickers and portfolio:
3013                self.CloseTrades(tickers, portfolio)
3014
3015            else:
3016                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3017
3018    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3019        """
3020        Universal method to create market or limit orders with all available parameters for current `accountId`.
3021        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3022
3023        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3024        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3025
3026        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3027        then broker immediately open market order as you can do simple --buy or --sell operations!
3028
3029        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3030        When current price will go up or down to target price value then broker opens a limit order.
3031        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3032
3033        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3034
3035        :param operation: string "Buy" or "Sell".
3036        :param orderType: string "Limit" or "Stop".
3037        :param lots: volume, integer count of lots >= 1.
3038        :param targetPrice: target price > 0. This is open trade price for limit order.
3039        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3040                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3041        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3042                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3043                         Stop loss order always executed by market price.
3044        :param expDate: string "Undefined" by default or local date in future.
3045                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3046                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3047                        A limit order has no expiration date, it lasts until the end of the trading day.
3048        :return: JSON with response from broker server.
3049        """
3050        if self.accountId is None or not self.accountId:
3051            uLogger.error("Variable `accountId` must be defined for using this method!")
3052            raise Exception("Account ID required")
3053
3054        if operation is None or not operation or operation not in ("Buy", "Sell"):
3055            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3056            raise Exception("Incorrect value")
3057
3058        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3059            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3060            raise Exception("Incorrect value")
3061
3062        if lots is None or lots < 1:
3063            uLogger.error("You must define trade volume > 0: integer count of lots!")
3064            raise Exception("Incorrect value")
3065
3066        if targetPrice is None or targetPrice <= 0:
3067            uLogger.error("Target price for limit-order must be greater than 0!")
3068            raise Exception("Incorrect value")
3069
3070        if limitPrice is None or limitPrice <= 0:
3071            limitPrice = targetPrice
3072
3073        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3074            stopType = "Limit"
3075
3076        if expDate is None or not expDate:
3077            expDate = "Undefined"
3078
3079        if not (self.ticker or self.figi):
3080            uLogger.error("Tocker or FIGI must be defined!")
3081            raise Exception("Ticker or FIGI required")
3082
3083        response = {}
3084        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3085        self.ticker = instrument["ticker"]
3086        self.figi = instrument["figi"]
3087
3088        if orderType == "Limit":
3089            uLogger.debug(
3090                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3091                    self.ticker, self.figi,
3092                    operation, lots, targetPrice, instrument["currency"],
3093                ))
3094
3095            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3096            self.body = str({
3097                "figi": self.figi,
3098                "quantity": str(lots),
3099                "price": FloatToNano(targetPrice),
3100                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3101                "accountId": str(self.accountId),
3102                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3103            })
3104            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3105
3106            if "orderId" in response.keys():
3107                uLogger.info(
3108                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3109                        response["orderId"],
3110                        self.ticker, self.figi,
3111                        operation, lots, targetPrice, instrument["currency"],
3112                    ))
3113
3114                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3115                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3116                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3117                            targetPrice, instrument["currency"],
3118                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3119                        ))
3120
3121                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3122                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3123                            targetPrice, instrument["currency"],
3124                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3125                        ))
3126
3127            else:
3128                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3129
3130        if orderType == "Stop":
3131            uLogger.debug(
3132                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3133                    self.ticker, self.figi,
3134                    operation, lots,
3135                    targetPrice, instrument["currency"],
3136                    limitPrice, instrument["currency"],
3137                    stopType, expDate,
3138                ))
3139
3140            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3141            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3142            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3143
3144            body = {
3145                "figi": self.figi,
3146                "quantity": str(lots),
3147                "price": FloatToNano(limitPrice),
3148                "stopPrice": FloatToNano(targetPrice),
3149                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3150                "accountId": str(self.accountId),
3151                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3152                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3153            }
3154
3155            if expDateUTC:
3156                body["expireDate"] = expDateUTC
3157
3158            self.body = str(body)
3159            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3160
3161            if "stopOrderId" in response.keys():
3162                uLogger.info(
3163                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3164                        response["stopOrderId"],
3165                        self.ticker, self.figi,
3166                        operation, lots,
3167                        targetPrice, instrument["currency"],
3168                        limitPrice, instrument["currency"],
3169                        TKS_STOP_ORDER_TYPES[stopOrderType],
3170                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3171                    ))
3172
3173                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3174                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3175                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3176                            targetPrice, instrument["currency"],
3177                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3178                        ))
3179
3180                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3181                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3182                            targetPrice, instrument["currency"],
3183                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3184                        ))
3185
3186            else:
3187                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3188
3189        return response
3190
3191    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3192        """
3193        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3194        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3195        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3196        See also: `Order()` docstring.
3197
3198        :param lots: volume, integer count of lots >= 1.
3199        :param targetPrice: target price > 0. This is open trade price for limit order.
3200        :return: JSON with response from broker server.
3201        """
3202        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3203
3204    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3205        """
3206        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3207        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3208        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3209        target price value then broker opens a limit order. See also: `Order()` docstring.
3210
3211        :param lots: volume, integer count of lots >= 1.
3212        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3213        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3214                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3215        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3216                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3217        :param expDate: string "Undefined" by default or local date in future.
3218                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3219                        This date is converting to UTC format for server.
3220        :return: JSON with response from broker server.
3221        """
3222        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3223
3224    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3225        """
3226        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3227        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3228        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3229        See also: `Order()` docstring.
3230
3231        :param lots: volume, integer count of lots >= 1.
3232        :param targetPrice: target price > 0. This is open trade price for limit order.
3233        :return: JSON with response from broker server.
3234        """
3235        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3236
3237    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3238        """
3239        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3240        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3241        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3242        target price value then broker opens a limit order. See also: `Order()` docstring.
3243
3244        :param lots: volume, integer count of lots >= 1.
3245        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3246        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3247                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3248        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3249                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3250        :param expDate: string "Undefined" by default or local date in future.
3251                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3252                        This date is converting to UTC format for server.
3253        :return: JSON with response from broker server.
3254        """
3255        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3256
3257    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3258        """
3259        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3260
3261        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3262        :param allOrdersIDs: pre-received lists of all active pending orders.
3263                             This avoids unnecessary downloading data from the server.
3264        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3265        """
3266        if self.accountId is None or not self.accountId:
3267            uLogger.error("Variable `accountId` must be defined for using this method!")
3268            raise Exception("Account ID required")
3269
3270        if orderIDs:
3271            if allOrdersIDs is None or not allOrdersIDs:
3272                rawOrders = self.RequestPendingOrders()
3273                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3274
3275            if allStopOrdersIDs is None or not allStopOrdersIDs:
3276                rawStopOrders = self.RequestStopOrders()
3277                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3278
3279            for orderID in orderIDs:
3280                idInPendingOrders = orderID in allOrdersIDs
3281                idInStopOrders = orderID in allStopOrdersIDs
3282
3283                if not (idInPendingOrders or idInStopOrders):
3284                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3285                    continue
3286
3287                else:
3288                    if idInPendingOrders:
3289                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3290
3291                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3292                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3293                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3294                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3295
3296                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3297                            if self.moreDebug:
3298                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3299
3300                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3301
3302                        else:
3303                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3304
3305                    elif idInStopOrders:
3306                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3307
3308                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3309                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3310                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3311                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3312
3313                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3314                            if self.moreDebug:
3315                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3316
3317                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3318
3319                        else:
3320                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3321
3322                    else:
3323                        continue
3324
3325    def CloseAllOrders(self) -> None:
3326        """
3327        Gets a list of open pending and stop orders and cancel it all.
3328        """
3329        rawOrders = self.RequestPendingOrders()
3330        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3331        lenOrders = len(allOrdersIDs)
3332
3333        rawStopOrders = self.RequestStopOrders()
3334        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3335        lenSOrders = len(allStopOrdersIDs)
3336
3337        if lenOrders > 0 or lenSOrders > 0:
3338            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3339
3340            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3341
3342        else:
3343            uLogger.info("Orders not found, nothing to cancel.")
3344
3345    def CloseAll(self, *args) -> None:
3346        """
3347        Close all available (not blocked) opened trades and orders.
3348
3349        Also, you can select one or more keywords case-insensitive:
3350        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3351
3352        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3353        """
3354        overview = self.Overview(show=False)  # get all open trades info
3355
3356        if len(args) == 0:
3357            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3358            self.CloseAllOrders()  # close all pending and stop orders
3359
3360            for iType in TKS_INSTRUMENTS:
3361                if iType != "Currencies":
3362                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3363
3364        else:
3365            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3366            lowerArgs = [x.lower() for x in args]
3367
3368            if "orders" in lowerArgs:
3369                self.CloseAllOrders()  # close all pending and stop orders
3370
3371            for iType in TKS_INSTRUMENTS:
3372                if iType.lower() in lowerArgs and iType != "Currencies":
3373                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3374
3375    @staticmethod
3376    def ParseOrderParameters(operation, **inputParameters):
3377        """
3378        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3379
3380        :param operation: string "Buy" or "Sell".
3381        :param inputParameters: this is dict of strings that looks like this
3382               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3383               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3384               "prices" key: one or more prices to open limit-orders
3385               Counts of values in lots and prices lists must be equals!
3386        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3387        """
3388        # TODO: update order grid work with api v2
3389        pass
3390        # uLogger.debug("Input parameters: {}".format(inputParameters))
3391        #
3392        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3393        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3394        #     raise Exception("Incorrect value")
3395        #
3396        # if "l" in inputParameters.keys():
3397        #     inputParameters["lots"] = inputParameters.pop("l")
3398        #
3399        # if "p" in inputParameters.keys():
3400        #     inputParameters["prices"] = inputParameters.pop("p")
3401        #
3402        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3403        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3404        #     raise Exception("Incorrect value")
3405        #
3406        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3407        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3408        #
3409        # if len(lots) != len(prices):
3410        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3411        #     raise Exception("Incorrect value")
3412        #
3413        # uLogger.debug("Extracted parameters for orders:")
3414        # uLogger.debug("lots = {}".format(lots))
3415        # uLogger.debug("prices = {}".format(prices))
3416        #
3417        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3418        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3419        # uLogger.debug("Order parameters: {}".format(result))
3420        #
3421        # return result
3422
3423    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3424        """
3425        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3426
3427        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3428        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3429        """
3430        result = False
3431        msg = "Instrument not defined!"
3432
3433        if portfolio is None or not portfolio:
3434            portfolio = self.Overview(show=False)
3435
3436        if self.ticker:
3437            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3438            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3439
3440            for iType in TKS_INSTRUMENTS:
3441                for instrument in portfolio["stat"][iType]:
3442                    if instrument["ticker"] == self.ticker:
3443                        result = True
3444                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3445                        break
3446
3447        elif self.figi:
3448            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3449            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3450
3451            for iType in TKS_INSTRUMENTS:
3452                for instrument in portfolio["stat"][iType]:
3453                    if instrument["figi"] == self.figi:
3454                        result = True
3455                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3456                        break
3457
3458        else:
3459            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3460
3461        uLogger.debug(msg)
3462
3463        return result
3464
3465    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3466        """
3467        Returns instrument from the user's portfolio if it presents there.
3468        Instrument must be defined by `ticker` (highly priority) or `figi`.
3469
3470        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3471        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3472        """
3473        result = None
3474        msg = "Instrument not defined!"
3475
3476        if portfolio is None or not portfolio:
3477            portfolio = self.Overview(show=False)
3478
3479        if self.ticker:
3480            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3481            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3482
3483            for iType in TKS_INSTRUMENTS:
3484                for instrument in portfolio["stat"][iType]:
3485                    if instrument["ticker"] == self.ticker:
3486                        result = instrument
3487                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3488                        break
3489
3490        elif self.figi:
3491            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3492            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3493
3494            for iType in TKS_INSTRUMENTS:
3495                for instrument in portfolio["stat"][iType]:
3496                    if instrument["figi"] == self.figi:
3497                        result = instrument
3498                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3499                        break
3500
3501        else:
3502            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3503
3504        uLogger.debug(msg)
3505
3506        return result
3507
3508    def RequestLimits(self) -> dict:
3509        """
3510        Method for obtaining the available funds for withdrawal for current `accountId`.
3511
3512        See also:
3513        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3514        - `OverviewLimits()` method
3515
3516        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3517                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3518                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3519                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3520        """
3521        if self.accountId is None or not self.accountId:
3522            uLogger.error("Variable `accountId` must be defined for using this method!")
3523            raise Exception("Account ID required")
3524
3525        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3526
3527        self.body = str({"accountId": self.accountId})
3528        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3529        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3530
3531        if self.moreDebug:
3532            uLogger.debug("Records about available funds for withdrawal successfully received")
3533
3534        return rawLimits
3535
3536    def OverviewLimits(self, show: bool = False) -> dict:
3537        """
3538        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3539
3540        See also: `RequestLimits()`.
3541
3542        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3543        :return: dict with raw parsed data from server and some calculated statistics about it.
3544        """
3545        if self.accountId is None or not self.accountId:
3546            uLogger.error("Variable `accountId` must be defined for using this method!")
3547            raise Exception("Account ID required")
3548
3549        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3550
3551        view = {
3552            "rawLimits": rawLimits,
3553            "limits": {  # parsed data for every currency:
3554                "money": {  # this is an array of portfolio currency positions
3555                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3556                },
3557                "blocked": {  # this is an array of blocked currency
3558                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3559                },
3560                "blockedGuarantee": {  # this is locked money under collateral for futures
3561                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3562                },
3563            },
3564        }
3565
3566        # --- Prepare text table with limits in human-readable format:
3567        if show:
3568            info = [
3569                "# Withdrawal limits\n\n",
3570                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3571                "* **Account ID:** [{}]\n".format(self.accountId),
3572            ]
3573
3574            if view["limits"]["money"]:
3575                info.extend([
3576                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3577                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3578                ])
3579
3580            else:
3581                info.append("\nNo withdrawal limits\n")
3582
3583            for curr in view["limits"]["money"].keys():
3584                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3585                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3586                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3587
3588                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3589                    "[{}]".format(curr),
3590                    "{:.2f}".format(view["limits"]["money"][curr]),
3591                    "{:.2f}".format(availableMoney),
3592                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3593                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3594                )
3595
3596                if curr == "rub":
3597                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3598
3599                else:
3600                    info.append(infoStr)
3601
3602            infoText = "".join(info)
3603
3604            uLogger.info(infoText)
3605
3606            if self.withdrawalLimitsFile:
3607                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3608                    fH.write(infoText)
3609
3610                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3611
3612        return view
3613
3614    def RequestAccounts(self) -> dict:
3615        """
3616        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3617
3618        See also:
3619        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3620        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3621        - `OverviewUserInfo()` method
3622
3623        :return: dict with raw data from server that contains accounts info. Example of dict:
3624                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3625                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3626                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3627                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3628        """
3629        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3630
3631        self.body = str({})
3632        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3633        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3634
3635        if self.moreDebug:
3636            uLogger.debug("Records about available accounts successfully received")
3637
3638        return rawAccounts
3639
3640    def RequestUserInfo(self) -> dict:
3641        """
3642        Method for requesting common user's information.
3643
3644        See also:
3645        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3646        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3647        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3648        - `OverviewUserInfo()` method
3649
3650        :return: dict with raw data from server that contains user's information. Example of dict:
3651                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3652                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3653        """
3654        uLogger.debug("Requesting common user's information. Wait, please...")
3655
3656        self.body = str({})
3657        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3658        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3659
3660        if self.moreDebug:
3661            uLogger.debug("Records about current user successfully received")
3662
3663        return rawUserInfo
3664
3665    def RequestMarginStatus(self, accountId: str = None) -> dict:
3666        """
3667        Method for requesting margin calculation for defined account ID.
3668
3669        See also:
3670        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3671        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3672        - `OverviewUserInfo()` method
3673
3674        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3675        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3676                 Example of responses:
3677                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3678                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3679                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3680                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3681                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3682                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3683        """
3684        if accountId is None or not accountId:
3685            if self.accountId is None or not self.accountId:
3686                uLogger.error("Variable `accountId` must be defined for using this method!")
3687                raise Exception("Account ID required")
3688
3689            else:
3690                accountId = self.accountId  # use `self.accountId` (main ID) by default
3691
3692        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3693
3694        self.body = str({"accountId": accountId})
3695        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3696        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3697
3698        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3699            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3700            rawMargin = {}
3701
3702        else:
3703            if self.moreDebug:
3704                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3705
3706        return rawMargin
3707
3708    def RequestTariffLimits(self) -> dict:
3709        """
3710        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3711
3712        See also:
3713        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3714        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3715        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3716        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3717        - `OverviewUserInfo()` method
3718
3719        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3720                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3721                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3722        """
3723        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3724
3725        self.body = str({})
3726        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3727        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3728
3729        if self.moreDebug:
3730            uLogger.debug("Records with limits of current tariff successfully received")
3731
3732        return rawTariffLimits
3733
3734    def RequestBondCoupons(self, iJSON: dict) -> dict:
3735        """
3736        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3737        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3738        All dates are in UTC timezone.
3739
3740        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3741        Documentation:
3742        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3743        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3744
3745        See also: `ExtendBondsData()`.
3746
3747        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3748                      If raw iJSON is not data of bond then server returns an error [400] with message:
3749                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3750        :return: dictionary with bond payment calendar. Response example
3751                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3752                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3753                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3754                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3755        """
3756        if iJSON["figi"] is None or not iJSON["figi"]:
3757            uLogger.error("FIGI must be defined for using this method!")
3758            raise Exception("FIGI required")
3759
3760        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3761        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3762
3763        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3764            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3765            self.figi,
3766            startDate,
3767            endDate,
3768        ))
3769
3770        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3771        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3772        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3773
3774        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3775            uLogger.warning("Instrument type is not bond!")
3776
3777        else:
3778            if self.moreDebug:
3779                uLogger.debug("Records about bond payment calendar successfully received")
3780
3781        return calendar
3782
3783    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3784        """
3785        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3786        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3787        coupon yields, current yields and some statistics etc.
3788
3789        WARNING! This is too long operation if a lot of bonds requested from broker server.
3790
3791        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3792
3793        :param instruments: list of strings with tickers or FIGIs.
3794        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3795                     for further used by data scientists or stock analytics.
3796        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3797                 In XLSX-file and Pandas DataFrame fields mean:
3798                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3799                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3800        """
3801        if instruments is None or not instruments:
3802            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3803            raise Exception("Ticker or FIGI required")
3804
3805        if isinstance(instruments, str):
3806            instruments = [instruments]
3807
3808        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3809
3810        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3811
3812        iCount = len(uniqueInstruments)
3813        tooLong = iCount >= 20
3814        if tooLong:
3815            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3816
3817        bonds = None
3818        for i, self.figi in enumerate(uniqueInstruments):
3819            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3820
3821            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3822                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3823                rawBond = self.SearchByFIGI(requestPrice=True)
3824
3825                # Widen raw data with UTC current time (iData["actualDateTime"]):
3826                actualDate = datetime.now(tzutc())
3827                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3828
3829                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3830                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3831
3832                # Replace some values with human-readable:
3833                iData["nominalCurrency"] = iData["nominal"]["currency"]
3834                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3835                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3836                iData["aciCurrency"] = iData["aciValue"]["currency"]
3837                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3838                iData["issueSize"] = int(iData["issueSize"])
3839                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3840                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3841                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3842                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3843                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3844                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3845                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3846                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3847                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3848                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3849
3850                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3851                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3852                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3853                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3854                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3855                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3856                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3857                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3858                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3859                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3860                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3861
3862                # Widen raw data with calendar data from `rawCalendar` values:
3863                calendarData = []
3864                if "events" in iData["rawCalendar"].keys():
3865                    for item in iData["rawCalendar"]["events"]:
3866                        calendarData.append({
3867                            "couponDate": item["couponDate"],
3868                            "couponNumber": int(item["couponNumber"]),
3869                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3870                            "payCurrency": item["payOneBond"]["currency"],
3871                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3872                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3873                            "couponStartDate": item["couponStartDate"],
3874                            "couponEndDate": item["couponEndDate"],
3875                            "couponPeriod": item["couponPeriod"],
3876                        })
3877
3878                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3879                    if "maturityDate" not in iData.keys():
3880                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3881
3882                # Widen raw data with Coupon Rate.
3883                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3884                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3885                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3886                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3887
3888                # Widen raw data with Yield to Maturity (YTM) on current date.
3889                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3890                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3891                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3892                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3893                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3894                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3895
3896                iData["calendar"] = calendarData  # adds calendar at the end
3897
3898                # Remove not used data:
3899                iData.pop("uid")
3900                iData.pop("positionUid")
3901                iData.pop("currentPrice")
3902                iData.pop("rawCalendar")
3903
3904                colNames = list(iData.keys())
3905                if bonds is None:
3906                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3907
3908                else:
3909                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3910
3911            else:
3912                uLogger.warning("Instrument is not a bond!")
3913
3914            processed = round(100 * (i + 1) / iCount, 1)
3915            if tooLong and processed % 5 == 0:
3916                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3917
3918            else:
3919                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3920
3921        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3922
3923        # Saving bonds from Pandas DataFrame to XLSX sheet:
3924        if xlsx and self.bondsXLSXFile:
3925            with pd.ExcelWriter(
3926                    path=self.bondsXLSXFile,
3927                    date_format=TKS_DATE_FORMAT,
3928                    datetime_format=TKS_DATE_TIME_FORMAT,
3929                    mode="w",
3930            ) as writer:
3931                bonds.to_excel(
3932                    writer,
3933                    sheet_name="Extended bonds data",
3934                    index=True,
3935                    encoding="UTF-8",
3936                    freeze_panes=(1, 1),
3937                )  # saving as XLSX-file with freeze first row and column as headers
3938
3939            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3940
3941        return bonds
3942
3943    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3944        """
3945        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3946
3947        WARNING! This is too long operation if a lot of bonds requested from broker server.
3948
3949        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3950
3951        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3952                        extended information about bonds: main info, current prices, bond payment calendar,
3953                        coupon yields, current yields and some statistics etc.
3954                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3955        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3956                     for further used by data scientists or stock analytics.
3957        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3958        """
3959        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3960            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3961
3962        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3963
3964        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3965        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3966        calendar = None
3967        for bond in extBonds.iterrows():
3968            for item in bond[1]["calendar"]:
3969                cData = {
3970                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3971                    "couponDate": item["couponDate"],
3972                    "figi": bond[1]["figi"],
3973                    "ticker": bond[1]["ticker"],
3974                    "name": bond[1]["name"],
3975                    "couponNumber": item["couponNumber"],
3976                    "payOneBond": item["payOneBond"],
3977                    "payCurrency": item["payCurrency"],
3978                    "couponType": item["couponType"],
3979                    "couponPeriod": item["couponPeriod"],
3980                    "fixDate": item["fixDate"],
3981                    "couponStartDate": item["couponStartDate"],
3982                    "couponEndDate": item["couponEndDate"],
3983                }
3984
3985                if calendar is None:
3986                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3987
3988                else:
3989                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
3990
3991        if calendar is not None:
3992            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
3993
3994            # Saving calendar from Pandas DataFrame to XLSX sheet:
3995            if xlsx:
3996                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
3997
3998                with pd.ExcelWriter(
3999                        path=xlsxCalendarFile,
4000                        date_format=TKS_DATE_FORMAT,
4001                        datetime_format=TKS_DATE_TIME_FORMAT,
4002                        mode="w",
4003                ) as writer:
4004                    humanReadable = calendar.copy(deep=True)
4005                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4006                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4007                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4008                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4009                    humanReadable.columns = colNames  # human-readable column names
4010
4011                    humanReadable.to_excel(
4012                        writer,
4013                        sheet_name="Bond payments calendar",
4014                        index=False,
4015                        encoding="UTF-8",
4016                        freeze_panes=(1, 2),
4017                    )  # saving as XLSX-file with freeze first row and column as headers
4018
4019                    del humanReadable  # release df in memory
4020
4021                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4022
4023        return calendar
4024
4025    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4026        """
4027        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4028        Also, creates Markdown file with calendar data, `calendar.md` by default.
4029
4030        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4031
4032        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4033                        extended information about bonds: main info, current prices, bond payment calendar,
4034                        coupon yields, current yields and some statistics etc.
4035                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4036        :param show: if `True` then also printing bonds payment calendar to the console,
4037                     otherwise save to file `calendarFile` only. `False` by default.
4038        :return: multilines text in Markdown format with bonds payment calendar as a table.
4039        """
4040        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4041            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4042
4043        infoText = "# Bond payments calendar\n\n"
4044
4045        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4046
4047        if not (calendar is None or calendar.empty):
4048            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4049
4050            info = [
4051                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4052                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4053            ]
4054
4055            newMonth = False
4056            notOneBond = calendar["figi"].nunique() > 1
4057            for i, bond in enumerate(calendar.iterrows()):
4058                if newMonth and notOneBond:
4059                    info.append(splitLine)
4060
4061                info.append(
4062                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4063                        "  √" if bond[1]["paid"] else "  —",
4064                        bond[1]["couponDate"].split("T")[0],
4065                        bond[1]["figi"],
4066                        bond[1]["ticker"],
4067                        bond[1]["couponNumber"],
4068                        "{} {}".format(
4069                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4070                            bond[1]["payCurrency"],
4071                        ),
4072                        bond[1]["couponType"],
4073                        bond[1]["couponPeriod"],
4074                        bond[1]["fixDate"].split("T")[0],
4075                    )
4076                )
4077
4078                if i < len(calendar.values) - 1:
4079                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4080                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4081                    newMonth = False if curDate.month == nextDate.month else True
4082
4083                else:
4084                    newMonth = False
4085
4086            infoText += "".join(info)
4087
4088            if show:
4089                uLogger.info("{}".format(infoText))
4090
4091            if self.calendarFile is not None:
4092                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4093                    fH.write(infoText)
4094
4095                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4096
4097        else:
4098            infoText += "No data\n"
4099
4100        return infoText
4101
4102    def OverviewAccounts(self, show: bool = False) -> dict:
4103        """
4104        Method for parsing and show simple table with all available user accounts.
4105
4106        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4107
4108        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4109        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4110                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4111                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4112                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4113                                                        "closed": "—", "access": "Full access" }, ...}}`
4114        """
4115        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4116
4117        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4118        accounts = {
4119            item["id"]: {
4120                "type": TKS_ACCOUNT_TYPES[item["type"]],
4121                "name": item["name"],
4122                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4123                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4124                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4125                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4126            } for item in rawAccounts["accounts"]
4127        }
4128
4129        # Raw and parsed data with some fields replaced in "stat" section:
4130        view = {
4131            "rawAccounts": rawAccounts,
4132            "stat": accounts,
4133        }
4134
4135        # --- Prepare simple text table with only accounts data in human-readable format:
4136        if show:
4137            info = [
4138                "# User accounts\n\n",
4139                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4140                "| Account ID   | Type                      | Status                    | Name                           |\n",
4141                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4142            ]
4143
4144            for account in view["stat"].keys():
4145                info.extend([
4146                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4147                        account,
4148                        view["stat"][account]["type"],
4149                        view["stat"][account]["status"],
4150                        view["stat"][account]["name"],
4151                    )
4152                ])
4153
4154            infoText = "".join(info)
4155
4156            uLogger.info(infoText)
4157
4158            if self.userAccountsFile:
4159                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4160                    fH.write(infoText)
4161
4162                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4163
4164        return view
4165
4166    def OverviewUserInfo(self, show: bool = False) -> dict:
4167        """
4168        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4169
4170        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4171
4172        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4173        :return: dict with raw parsed data from server and some calculated statistics about it.
4174        """
4175        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4176        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4177        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4178        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4179        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4180        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4181
4182        # This is dict with parsed common user data:
4183        userInfo = {
4184            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4185            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4186            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4187            "tariff": rawUserInfo["tariff"],
4188        }
4189
4190        # This is an array of dict with parsed margin statuses for every account IDs:
4191        margins = {}
4192        for accountId in accounts.keys():
4193            if rawMargins[accountId]:
4194                margins[accountId] = {
4195                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4196                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4197                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4198                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4199                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4200                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4201                }
4202
4203            else:
4204                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4205
4206        unary = {}  # unary-connection limits
4207        for item in rawTariffLimits["unaryLimits"]:
4208            if item["limitPerMinute"] in unary.keys():
4209                unary[item["limitPerMinute"]].extend(item["methods"])
4210
4211            else:
4212                unary[item["limitPerMinute"]] = item["methods"]
4213
4214        stream = {}  # stream-connection limits
4215        for item in rawTariffLimits["streamLimits"]:
4216            if item["limit"] in stream.keys():
4217                stream[item["limit"]].extend(item["streams"])
4218
4219            else:
4220                stream[item["limit"]] = item["streams"]
4221
4222        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4223        limits = {
4224            "unary": unary,
4225            "stream": stream,
4226        }
4227
4228        # Raw and parsed data as an output result:
4229        view = {
4230            "rawUserInfo": rawUserInfo,
4231            "rawAccounts": rawAccounts,
4232            "rawMargins": rawMargins,
4233            "rawTariffLimits": rawTariffLimits,
4234            "stat": {
4235                "userInfo": userInfo,
4236                "accounts": accounts,
4237                "margins": margins,
4238                "limits": limits,
4239            },
4240        }
4241
4242        # --- Prepare text table with user information in human-readable format:
4243        if show:
4244            info = [
4245                "# Full user information\n\n",
4246                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4247                "## Common information\n\n",
4248                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4249                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4250                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4251                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4252                "\n## User accounts\n\n",
4253            ]
4254
4255            for account in view["stat"]["accounts"].keys():
4256                info.extend([
4257                    "### ID: [{}]\n\n".format(account),
4258                    "| Parameters           | Values                                                       |\n",
4259                    "|----------------------|--------------------------------------------------------------|\n",
4260                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4261                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4262                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4263                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4264                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4265                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4266                ])
4267
4268                if margins[account]:
4269                    info.extend([
4270                        "| Margin status:       | Enabled                                                      |\n",
4271                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4272                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4273                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4274                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4275                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4276                    ])
4277
4278                else:
4279                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4280
4281            info.extend([
4282                "\n## Current user tariff limits\n",
4283                "\nSee also:\n",
4284                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4285                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4286                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4287                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4288                "\n### Unary limits\n",
4289            ])
4290
4291            if unary:
4292                for key, values in sorted(unary.items()):
4293                    info.append("\n* Max requests per minute: {}\n".format(key))
4294
4295                    for value in values:
4296                        info.append("  - {}\n".format(value))
4297
4298            else:
4299                info.append("\nNot available\n")
4300
4301            info.append("\n### Stream limits\n")
4302
4303            if stream:
4304                for key, values in sorted(stream.items()):
4305                    info.append("\n* Max stream connections: {}\n".format(key))
4306
4307                    for value in values:
4308                        info.append("  - {}\n".format(value))
4309
4310            else:
4311                info.append("\nNot available\n")
4312
4313            infoText = "".join(info)
4314
4315            uLogger.info(infoText)
4316
4317            if self.userInfoFile:
4318                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4319                    fH.write(infoText)
4320
4321                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4322
4323        return view
4324
4325
4326class Args:
4327    """
4328    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4329    """
4330    def __init__(self, **kwargs):
4331        self.__dict__.update(kwargs)
4332
4333    def __getattr__(self, item):
4334        return None
4335
4336
4337def ParseArgs():
4338    """This function get and parse command line keys."""
4339    parser = ArgumentParser()  # command-line string parser
4340
4341    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4342    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4343
4344    # --- options:
4345
4346    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4347    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4348    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4349
4350    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4351    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4352
4353    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4354    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4355
4356    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4357
4358    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4359    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4360    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4361
4362    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4363    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4364
4365    # --- commands:
4366
4367    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4368
4369    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4370    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4371    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4372    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4373    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4374    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4375    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4376    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4377
4378    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4379    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4380    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4381    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4382    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4383    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4384
4385    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4386    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4387    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4388    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4389
4390    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4391    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4392    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4393
4394    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4395    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4396    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4397    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4398    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4399    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4400    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4401
4402    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4403    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4404    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4405    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4406    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4407
4408    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4409    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4410    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4411
4412    cmdArgs = parser.parse_args()
4413    return cmdArgs
4414
4415
4416def Main(**kwargs):
4417    """
4418    Main function for work with TKSBrokerAPI in the console.
4419
4420    See examples:
4421    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4422    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4423    """
4424    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4425
4426    if args.debug_level:
4427        uLogger.level = 10  # always debug level by default
4428        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4429
4430    exitCode = 0
4431    start = datetime.now(tzutc())
4432    uLogger.debug("=-" * 50)
4433    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4434        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4435        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4436    ))
4437
4438    # trying to calculate full current version:
4439    buildVersion = __version__
4440    try:
4441        v = version("tksbrokerapi")
4442        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4443
4444    except Exception:
4445        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4446
4447    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4448    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4449
4450    try:
4451        if args.version:
4452            print("TKSBrokerAPI {}".format(buildVersion))
4453            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4454
4455        else:
4456            # Init class for trading with Tinkoff Broker:
4457            trader = TinkoffBrokerServer(
4458                token=args.token,
4459                accountId=args.account_id,
4460                useCache=not args.no_cache,
4461            )
4462
4463            # --- set some options:
4464
4465            if args.more:
4466                trader.moreDebug = True
4467                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4468
4469            if args.ticker:
4470                ticker = args.ticker.upper()  # Tickers may be upper case only
4471
4472                if ticker in trader.aliasesKeys:
4473                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4474
4475                else:
4476                    trader.ticker = ticker
4477
4478            if args.figi:
4479                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4480
4481            if args.depth is not None:
4482                trader.depth = args.depth
4483
4484            # --- do one command:
4485
4486            if args.list:
4487                if args.output is not None:
4488                    trader.instrumentsFile = args.output
4489
4490                trader.ShowInstrumentsInfo(show=True)
4491
4492            elif args.list_xlsx:
4493                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4494
4495            elif args.bonds_xlsx is not None:
4496                if args.output is not None:
4497                    trader.bondsXLSXFile = args.output
4498
4499                if len(args.bonds_xlsx) == 0:
4500                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4501
4502                else:
4503                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4504
4505            elif args.search:
4506                if args.output is not None:
4507                    trader.searchResultsFile = args.output
4508
4509                trader.SearchInstruments(pattern=args.search[0], show=True)
4510
4511            elif args.info:
4512                if not (args.ticker or args.figi):
4513                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4514                    raise Exception("Ticker or FIGI required")
4515
4516                if args.output is not None:
4517                    trader.infoFile = args.output
4518
4519                if args.ticker:
4520                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4521
4522                else:
4523                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4524
4525            elif args.calendar is not None:
4526                if args.output is not None:
4527                    trader.calendarFile = args.output
4528
4529                if len(args.calendar) == 0:
4530                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4531
4532                else:
4533                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4534
4535                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4536
4537            elif args.price:
4538                if not (args.ticker or args.figi):
4539                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4540                    raise Exception("Ticker or FIGI required")
4541
4542                trader.GetCurrentPrices(show=True)
4543
4544            elif args.prices is not None:
4545                if args.output is not None:
4546                    trader.pricesFile = args.output
4547
4548                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4549
4550            elif args.overview:
4551                if args.output is not None:
4552                    trader.overviewFile = args.output
4553
4554                trader.Overview(show=True, details="full")
4555
4556            elif args.overview_digest:
4557                if args.output is not None:
4558                    trader.overviewDigestFile = args.output
4559
4560                trader.Overview(show=True, details="digest")
4561
4562            elif args.overview_positions:
4563                if args.output is not None:
4564                    trader.overviewPositionsFile = args.output
4565
4566                trader.Overview(show=True, details="positions")
4567
4568            elif args.overview_orders:
4569                if args.output is not None:
4570                    trader.overviewOrdersFile = args.output
4571
4572                trader.Overview(show=True, details="orders")
4573
4574            elif args.overview_analytics:
4575                if args.output is not None:
4576                    trader.overviewAnalyticsFile = args.output
4577
4578                trader.Overview(show=True, details="analytics")
4579
4580            elif args.overview_calendar:
4581                if args.output is not None:
4582                    trader.overviewAnalyticsFile = args.output
4583
4584                trader.Overview(show=True, details="calendar")
4585
4586            elif args.deals is not None:
4587                if args.output is not None:
4588                    trader.reportFile = args.output
4589
4590                if 0 <= len(args.deals) < 3:
4591                    trader.Deals(
4592                        start=args.deals[0] if len(args.deals) >= 1 else None,
4593                        end=args.deals[1] if len(args.deals) == 2 else None,
4594                        show=True,  # Always show deals report in console
4595                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4596                    )
4597
4598                else:
4599                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4600                    raise Exception("Incorrect value")
4601
4602            elif args.history is not None:
4603                if args.output is not None:
4604                    trader.historyFile = args.output
4605
4606                if 0 <= len(args.history) < 3:
4607                    dataReceived = trader.History(
4608                        start=args.history[0] if len(args.history) >= 1 else None,
4609                        end=args.history[1] if len(args.history) == 2 else None,
4610                        interval="hour" if args.interval is None or not args.interval else args.interval,
4611                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4612                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4613                        show=True,  # shows all downloaded candles in console
4614                    )
4615
4616                    if args.render_chart is not None and dataReceived is not None:
4617                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4618
4619                        trader.ShowHistoryChart(
4620                            candles=dataReceived,
4621                            interact=iChart,
4622                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4623                        )
4624
4625                else:
4626                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4627                    raise Exception("Incorrect value")
4628
4629            elif args.load_history is not None:
4630                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4631
4632                if args.render_chart is not None and histData is not None:
4633                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4634                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4635
4636                    trader.ShowHistoryChart(
4637                        candles=histData,
4638                        interact=iChart,
4639                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4640                    )
4641
4642            elif args.trade is not None:
4643                if 1 <= len(args.trade) <= 5:
4644                    trader.Trade(
4645                        operation=args.trade[0],
4646                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4647                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4648                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4649                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4650                    )
4651
4652                else:
4653                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4654
4655            elif args.buy is not None:
4656                if 0 <= len(args.buy) <= 4:
4657                    trader.Buy(
4658                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4659                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4660                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4661                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4662                    )
4663
4664                else:
4665                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4666
4667            elif args.sell is not None:
4668                if 0 <= len(args.sell) <= 4:
4669                    trader.Sell(
4670                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4671                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4672                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4673                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4674                    )
4675
4676                else:
4677                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4678
4679            elif args.order:
4680                if 4 <= len(args.order) <= 7:
4681                    trader.Order(
4682                        operation=args.order[0],
4683                        orderType=args.order[1],
4684                        lots=int(args.order[2]),
4685                        targetPrice=float(args.order[3]),
4686                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4687                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4688                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4689                    )
4690
4691                else:
4692                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4693
4694            elif args.buy_limit:
4695                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4696
4697            elif args.sell_limit:
4698                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4699
4700            elif args.buy_stop:
4701                if 2 <= len(args.buy_stop) <= 7:
4702                    trader.BuyStop(
4703                        lots=int(args.buy_stop[0]),
4704                        targetPrice=float(args.buy_stop[1]),
4705                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4706                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4707                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4708                    )
4709
4710                else:
4711                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4712
4713            elif args.sell_stop:
4714                if 2 <= len(args.sell_stop) <= 7:
4715                    trader.SellStop(
4716                        lots=int(args.sell_stop[0]),
4717                        targetPrice=float(args.sell_stop[1]),
4718                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4719                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4720                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4721                    )
4722
4723                else:
4724                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4725
4726            # elif args.buy_order_grid is not None:
4727            #     # update order grid work with api v2
4728            #     if len(args.buy_order_grid) == 2:
4729            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4730            #
4731            #         for order in orderParams:
4732            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4733            #
4734            #     else:
4735            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4736            #
4737            # elif args.sell_order_grid is not None:
4738            #     # update order grid work with api v2
4739            #     if len(args.sell_order_grid) >= 2:
4740            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4741            #
4742            #         for order in orderParams:
4743            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4744            #
4745            #     else:
4746            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4747
4748            elif args.close_order is not None:
4749                trader.CloseOrders(args.close_order)  # close only one order
4750
4751            elif args.close_orders is not None:
4752                trader.CloseOrders(args.close_orders)  # close list of orders
4753
4754            elif args.close_trade:
4755                if not (args.ticker or args.figi):
4756                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4757                    raise Exception("Ticker or FIGI required")
4758
4759                if args.ticker:
4760                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4761
4762                else:
4763                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4764
4765            elif args.close_trades is not None:
4766                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4767
4768            elif args.close_all is not None:
4769                trader.CloseAll(*args.close_all)
4770
4771            elif args.limits:
4772                if args.output is not None:
4773                    trader.withdrawalLimitsFile = args.output
4774
4775                trader.OverviewLimits(show=True)
4776
4777            elif args.user_info:
4778                if args.output is not None:
4779                    trader.userInfoFile = args.output
4780
4781                trader.OverviewUserInfo(show=True)
4782
4783            elif args.account:
4784                if args.output is not None:
4785                    trader.userAccountsFile = args.output
4786
4787                trader.OverviewAccounts(show=True)
4788
4789            else:
4790                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4791                raise Exception("There is no command to execute")
4792
4793    except Exception:
4794        trace = tb.format_exc()
4795        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4796            if e in trace:
4797                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4798                break
4799
4800        uLogger.debug(trace)
4801        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4802        exitCode = 255  # an error occurred, must be open a ticket for this issue
4803
4804    finally:
4805        finish = datetime.now(tzutc())
4806
4807        if exitCode == 0:
4808            if args.more:
4809                uLogger.debug("All operations were finished success (summary code is 0).")
4810
4811        else:
4812            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4813                os.path.abspath(uLog.defaultLogFile), exitCode,
4814            ))
4815
4816        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4817        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4818            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4819            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4820        ))
4821        uLogger.debug("=-" * 50)
4822
4823        if not kwargs:
4824            sys.exit(exitCode)
4825
4826        else:
4827            return exitCode
4828
4829
4830if __name__ == "__main__":
4831    Main()
class TinkoffBrokerServer:
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.aliases = TKS_TICKER_ALIASES
 129        """Some aliases instead official tickers.
 130
 131        See also: `TKSEnums.TKS_TICKER_ALIASES`
 132        """
 133
 134        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 135
 136        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 137
 138        self.ticker = ""
 139        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 140
 141        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 142        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 143
 144        See also: `SearchByTicker()`, `SearchInstruments()`.
 145        """
 146
 147        self.figi = ""
 148        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 149
 150        See also: `SearchByFIGI()`, `SearchInstruments()`.
 151        """
 152
 153        self.depth = 1
 154        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 155
 156        See also: `GetCurrentPrices()`.
 157        """
 158
 159        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 160        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 161
 162        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 163        """
 164
 165        uLogger.debug("Broker API server: {}".format(self.server))
 166
 167        self.timeout = 15
 168        """Server operations timeout in seconds. Default: `15`.
 169
 170        See also: `SendAPIRequest()`.
 171        """
 172
 173        self.headers = {
 174            "Content-Type": "application/json",
 175            "accept": "application/json",
 176            "Authorization": "Bearer {}".format(self.token),
 177            "x-app-name": "Tim55667757.TKSBrokerAPI",
 178        }
 179        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 180
 181        See also: `SendAPIRequest()`.
 182        """
 183
 184        self.body = None
 185        """Request body which send to broker server. Default: `None`.
 186
 187        See also: `SendAPIRequest()`.
 188        """
 189
 190        self.moreDebug = False
 191        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 192
 193        self.historyFile = None
 194        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 195
 196        See also: `History()`.
 197        """
 198
 199        self.htmlHistoryFile = "index.html"
 200        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 201
 202        See also: `ShowHistoryChart()`.
 203        """
 204
 205        self.instrumentsFile = "instruments.md"
 206        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 207
 208        See also: `ShowInstrumentsInfo()`.
 209        """
 210
 211        self.searchResultsFile = "search-results.md"
 212        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 213
 214        See also: `SearchInstruments()`.
 215        """
 216
 217        self.pricesFile = "prices.md"
 218        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 219
 220        See also: `GetListOfPrices()`.
 221        """
 222
 223        self.infoFile = "info.md"
 224        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 225
 226        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 227        """
 228
 229        self.bondsXLSXFile = "ext-bonds.xlsx"
 230        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 231        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 232
 233        See also: `ExtendBondsData()`.
 234        """
 235
 236        self.calendarFile = "calendar.md"
 237        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 238        
 239        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 240
 241        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 242        """
 243
 244        self.overviewFile = "overview.md"
 245        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 246
 247        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 248        """
 249
 250        self.overviewDigestFile = "overview-digest.md"
 251        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 252
 253        See also: `Overview()` with parameter `details="digest"`.
 254        """
 255
 256        self.overviewPositionsFile = "overview-positions.md"
 257        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 258
 259        See also: `Overview()` with parameter `details="positions"`.
 260        """
 261
 262        self.overviewOrdersFile = "overview-orders.md"
 263        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 264
 265        See also: `Overview()` with parameter `details="orders"`.
 266        """
 267
 268        self.overviewAnalyticsFile = "overview-analytics.md"
 269        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 270
 271        See also: `Overview()` with parameter `details="analytics"`.
 272        """
 273
 274        self.overviewBondsCalendarFile = "overview-calendar.md"
 275        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 276
 277        See also: `Overview()` with parameter `details="calendar"`.
 278        """
 279
 280        self.reportFile = "deals.md"
 281        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 282
 283        See also: `Deals()`.
 284        """
 285
 286        self.withdrawalLimitsFile = "limits.md"
 287        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 288
 289        See also: `OverviewLimits()` and `RequestLimits()`.
 290        """
 291
 292        self.userInfoFile = "user-info.md"
 293        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 294
 295        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 296        """
 297
 298        self.userAccountsFile = "accounts.md"
 299        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 300
 301        See also: `OverviewAccounts()`, `RequestAccounts()`.
 302        """
 303
 304        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 305        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 306
 307        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 308
 309        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 310        """
 311
 312        self.iList = None  # init iList for raw instruments data
 313        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 314        
 315        See also: `Listing()`, `DumpInstruments()`.
 316        """
 317
 318        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 319        if useCache:
 320            if os.path.exists(self.iListDumpFile):
 321                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 322                curTime = datetime.now(tzutc())
 323
 324                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 325                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 326
 327                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 328
 329                else:
 330                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 331
 332                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 333                        os.path.abspath(self.iListDumpFile),
 334                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 335                    ))
 336
 337            else:
 338                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 339                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 340
 341        else:
 342            self.iList = self.Listing()  # request new raw instruments data from broker server
 343            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 344
 345        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 346        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 347
 348        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 349        """
 350
 351    def _ParseJSON(self, rawData="{}") -> dict:
 352        """
 353        Parse JSON from response string.
 354
 355        :param rawData: this is a string with JSON-formatted text.
 356        :return: JSON (dictionary), parsed from server response string.
 357        """
 358        responseJSON = json.loads(rawData) if rawData else {}
 359
 360        if self.moreDebug:
 361            uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 362
 363        return responseJSON
 364
 365    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 366        """
 367        Send GET or POST request to broker server and receive JSON object.
 368
 369        self.header: must be defining with dictionary of headers.
 370        self.body: if define then used as request body. None by default.
 371        self.timeout: global request timeout, 15 seconds by default.
 372        :param url: url with REST request.
 373        :param reqType: send "GET" or "POST" request. "GET" by default.
 374        :param retry: how many times retry after first request if an 5xx server errors occurred.
 375        :param pause: sleep time in seconds between retries.
 376        :return: response JSON (dictionary) from broker.
 377        """
 378        if reqType not in ("GET", "POST"):
 379            uLogger.error("You can define request type: 'GET' or 'POST'!")
 380            raise Exception("Incorrect value")
 381
 382        if self.moreDebug:
 383            uLogger.debug("Request parameters:")
 384            uLogger.debug("    - REST API URL: {}".format(url))
 385            uLogger.debug("    - request type: {}".format(reqType))
 386            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 387            uLogger.debug("    - body:\n{}".format(self.body))
 388
 389        # fast hack to avoid all operations with some tickers/FIGI
 390        responseJSON = {}
 391        oK = True
 392        for item in self.exclude:
 393            if item in url:
 394                if self.moreDebug:
 395                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 396
 397                oK = False
 398                break
 399
 400        if oK:
 401            counter = 0
 402            response = None
 403            errMsg = ""
 404
 405            while not response and counter <= retry:
 406                if reqType == "GET":
 407                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 408
 409                if reqType == "POST":
 410                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 411
 412                if self.moreDebug:
 413                    uLogger.debug("Response:")
 414                    uLogger.debug("    - status code: {}".format(response.status_code))
 415                    uLogger.debug("    - reason: {}".format(response.reason))
 416                    uLogger.debug("    - body length: {}".format(len(response.text)))
 417                    uLogger.debug("    - headers:\n{}".format(response.headers))
 418
 419                # Server returns some headers:
 420                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 421                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 422                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 423                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 424                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 425                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
 426                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 427                    sleep(rateLimitWait)
 428
 429                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 430                if 400 <= response.status_code < 500:
 431                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 432                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 433                    counter = retry + 1
 434
 435                if 500 <= response.status_code < 600:
 436                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 437                    uLogger.debug("    - not oK, {}".format(errMsg))
 438                    counter += 1
 439
 440                    if counter <= retry:
 441                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 442                        sleep(pause)
 443
 444            responseJSON = self._ParseJSON(rawData=response.text)
 445
 446            if errMsg:
 447                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 448                uLogger.error("    - not oK, {}".format(errMsg))
 449
 450        return responseJSON
 451
 452    def _IUpdater(self, iType: str) -> tuple:
 453        """
 454        Request instrument by type from server. See available API methods for instruments:
 455        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 456        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 457        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 458        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 459        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 460
 461        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 462        :return: tuple with iType name and list of available instruments of current type for defined user token.
 463        """
 464        result = []
 465
 466        if iType in TKS_INSTRUMENTS:
 467            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 468
 469            # all instruments have the same body in API v2 requests:
 470            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 471            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 472            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 473
 474        return iType, result
 475
 476    def _IWrapper(self, kwargs):
 477        """
 478        Wrapper runs instrument's update method `_IUpdater()`.
 479        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 480        """
 481        return self._IUpdater(**kwargs)
 482
 483    def Listing(self) -> dict:
 484        """
 485        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 486
 487        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 488        """
 489        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 490        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 491
 492        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 493        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 494        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 495
 496        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 497        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 498        poolUpdater.close()
 499
 500        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 501        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 502        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 503
 504        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 505        for iType in iList.keys():
 506            for ticker in iList[iType]:
 507                iList[iType][ticker]["type"] = iType
 508
 509                if "minPriceIncrement" in iList[iType][ticker].keys():
 510                    iList[iType][ticker]["step"] = NanoToFloat(
 511                        iList[iType][ticker]["minPriceIncrement"]["units"],
 512                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 513                    )
 514
 515                else:
 516                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 517
 518        return iList
 519
 520    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 521        """
 522        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 523
 524        See also: `DumpInstruments()`, `Listing()`.
 525
 526        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 527                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 528        """
 529        if self.iListDumpFile is None or not self.iListDumpFile:
 530            uLogger.error("Output name of dump file must be defined!")
 531            raise Exception("Filename required")
 532
 533        if not self.iList or forceUpdate:
 534            self.iList = self.Listing()
 535
 536        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 537
 538        # Save as XLSX with separated sheets for every type of instruments:
 539        with pd.ExcelWriter(
 540                path=xlsxDumpFile,
 541                date_format=TKS_DATE_FORMAT,
 542                datetime_format=TKS_DATE_TIME_FORMAT,
 543                mode="w",
 544        ) as writer:
 545            for iType in TKS_INSTRUMENTS:
 546                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 547                df = df[sorted(df)]  # sorted by column names
 548                df = df.applymap(
 549                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 550                    na_action="ignore",
 551                )  # converting numbers from nano-type to float in every cell
 552                df.to_excel(
 553                    writer,
 554                    sheet_name=iType,
 555                    encoding="UTF-8",
 556                    freeze_panes=(1, 1),
 557                )  # saving as XLSX-file with freeze first row and column as headers
 558
 559        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 560
 561    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 562        """
 563        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 564        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 565
 566        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 567
 568        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 569                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 570        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 571        """
 572        if self.iListDumpFile is None or not self.iListDumpFile:
 573            uLogger.error("Output name of dump file must be defined!")
 574            raise Exception("Filename required")
 575
 576        if not self.iList or forceUpdate:
 577            self.iList = self.Listing()
 578
 579        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 580        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 581            fH.write(jsonDump)
 582
 583        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 584
 585        return jsonDump
 586
 587    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 588        """
 589        Show information about one instrument defined by json data and prints it in Markdown format.
 590
 591        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 592
 593        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
 594        :param show: if `True` then also printing information about instrument and its current price.
 595        :return: multilines text in Markdown format with information about one instrument.
 596        """
 597        splitLine = "|                                                             |                                                        |\n"
 598        infoText = ""
 599
 600        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 601            info = [
 602                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
 603                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
 604                "| Parameters                                                  | Values                                                 |\n",
 605                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 606                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 607                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 608            ]
 609
 610            if "sector" in iJSON.keys() and iJSON["sector"]:
 611                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 612
 613            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 614                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 615
 616            info.extend([
 617                splitLine,
 618                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 619                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 620            ])
 621
 622            if "isin" in iJSON.keys() and iJSON["isin"]:
 623                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 624
 625            if "classCode" in iJSON.keys():
 626                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 627
 628            info.extend([
 629                splitLine,
 630                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 631                splitLine,
 632                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 633                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 634                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 635            ])
 636
 637            if iJSON["figi"]:
 638                self.figi = iJSON["figi"]
 639                iJSON = iJSON | self.RequestTradingStatus()
 640
 641                info.extend([
 642                    splitLine,
 643                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 644                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 645                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 646                ])
 647
 648            info.append(splitLine)
 649
 650            if "type" in iJSON.keys() and iJSON["type"]:
 651                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 652
 653                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 654                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 655
 656            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 657                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 658
 659            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 660                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 661
 662            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 663                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 664
 665            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 666                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 667
 668            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 669                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 670
 671            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 672                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 673
 674            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 675                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 676
 677            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 678                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 679
 680            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 681                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 682
 683            if "currency" in iJSON.keys():
 684                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 685
 686            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 687                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 688
 689            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 690                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 691
 692            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 693                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 694
 695            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 696                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 697
 698            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 699                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 700
 701            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 702                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 703
 704            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 705                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 706
 707            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 708                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 709
 710            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 711                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 712
 713            iExt = None
 714            if iJSON["type"] == "Bonds":
 715                info.extend([
 716                    splitLine,
 717                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 718                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 719                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 720                        iJSON["nominal"]["currency"],
 721                    )),
 722                ])
 723
 724                if "floatingCouponFlag" in iJSON.keys():
 725                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 726
 727                if "amortizationFlag" in iJSON.keys():
 728                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 729
 730                info.append(splitLine)
 731
 732                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 733                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 734
 735                if iJSON["figi"]:
 736                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 737
 738                    info.extend([
 739                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 740                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 741                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 742                    ])
 743
 744                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 745                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 746                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 747                        iJSON["aciValue"]["currency"]
 748                    )))
 749
 750            if "currentPrice" in iJSON.keys():
 751                info.append(splitLine)
 752
 753                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 754                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 755
 756                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 757                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 758                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 759                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 760                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 761
 762                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 763                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 764
 765                info.extend([
 766                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 767                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 768                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 769                    )),
 770                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 771                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 772                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 773                    )),
 774                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 775                        "{:.2f}%{}".format(
 776                            iJSON["currentPrice"]["changes"],
 777                            " ({}{:.2f} {})".format(
 778                                "+" if bondChangesDelta > 0 else "",
 779                                bondChangesDelta,
 780                                aciCurrency
 781                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 782                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 783                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 784                                currency
 785                            ),
 786                        )
 787                    ),
 788                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 789                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 790                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 791                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 792                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 793                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 794                    )),
 795                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 796                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 797                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 798                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 799                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 800                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 801                    )),
 802                ])
 803
 804            if "lot" in iJSON.keys():
 805                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 806
 807            if "step" in iJSON.keys() and iJSON["step"] != 0:
 808                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 809
 810            # Add bond payment calendar:
 811            if iJSON["type"] == "Bonds":
 812                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 813                info.extend(["\n", strCalendar])
 814
 815            infoText += "".join(info)
 816
 817            if show:
 818                uLogger.info("{}".format(infoText))
 819
 820            else:
 821                uLogger.debug("{}".format(infoText))
 822
 823            if self.infoFile is not None:
 824                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 825                    fH.write(infoText)
 826
 827                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 828
 829        return infoText
 830
 831    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 832        """
 833        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 834
 835        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 836        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 837        :return: JSON formatted data with information about instrument.
 838        """
 839        tickerJSON = {}
 840        if self.moreDebug:
 841            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
 842
 843        if not self.ticker:
 844            uLogger.warning("self.ticker variable is not be empty!")
 845
 846        else:
 847            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 848                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
 849                raise Exception("Instrument not allowed")
 850
 851            if not self.iList:
 852                self.iList = self.Listing()
 853
 854            if self.ticker in self.iList["Shares"].keys():
 855                tickerJSON = self.iList["Shares"][self.ticker]
 856                if self.moreDebug:
 857                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
 858
 859            elif self.ticker in self.iList["Currencies"].keys():
 860                tickerJSON = self.iList["Currencies"][self.ticker]
 861                if self.moreDebug:
 862                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
 863
 864            elif self.ticker in self.iList["Bonds"].keys():
 865                tickerJSON = self.iList["Bonds"][self.ticker]
 866                if self.moreDebug:
 867                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
 868
 869            elif self.ticker in self.iList["Etfs"].keys():
 870                tickerJSON = self.iList["Etfs"][self.ticker]
 871                if self.moreDebug:
 872                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
 873
 874            elif self.ticker in self.iList["Futures"].keys():
 875                tickerJSON = self.iList["Futures"][self.ticker]
 876                if self.moreDebug:
 877                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
 878
 879        if tickerJSON:
 880            self.figi = tickerJSON["figi"]
 881
 882            if requestPrice:
 883                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 884
 885                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 886                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 887
 888                else:
 889                    tickerJSON["currentPrice"]["changes"] = 0
 890
 891            if show:
 892                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 893
 894        else:
 895            if show:
 896                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
 897
 898        return tickerJSON
 899
 900    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 901        """
 902        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 903
 904        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 905        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 906        :return: JSON formatted data with information about instrument.
 907        """
 908        figiJSON = {}
 909        if self.moreDebug:
 910            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
 911
 912        if not self.figi:
 913            uLogger.warning("self.figi variable is not be empty!")
 914
 915        else:
 916            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 917                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
 918                raise Exception("Instrument not allowed")
 919
 920            if not self.iList:
 921                self.iList = self.Listing()
 922
 923            for item in self.iList["Shares"].keys():
 924                if self.figi == self.iList["Shares"][item]["figi"]:
 925                    figiJSON = self.iList["Shares"][item]
 926
 927                    if self.moreDebug:
 928                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
 929
 930                    break
 931
 932            if not figiJSON:
 933                for item in self.iList["Currencies"].keys():
 934                    if self.figi == self.iList["Currencies"][item]["figi"]:
 935                        figiJSON = self.iList["Currencies"][item]
 936
 937                        if self.moreDebug:
 938                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
 939
 940                        break
 941
 942            if not figiJSON:
 943                for item in self.iList["Bonds"].keys():
 944                    if self.figi == self.iList["Bonds"][item]["figi"]:
 945                        figiJSON = self.iList["Bonds"][item]
 946
 947                        if self.moreDebug:
 948                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
 949
 950                        break
 951
 952            if not figiJSON:
 953                for item in self.iList["Etfs"].keys():
 954                    if self.figi == self.iList["Etfs"][item]["figi"]:
 955                        figiJSON = self.iList["Etfs"][item]
 956
 957                        if self.moreDebug:
 958                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
 959
 960                        break
 961
 962            if not figiJSON:
 963                for item in self.iList["Futures"].keys():
 964                    if self.figi == self.iList["Futures"][item]["figi"]:
 965                        figiJSON = self.iList["Futures"][item]
 966
 967                        if self.moreDebug:
 968                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
 969
 970                        break
 971
 972        if figiJSON:
 973            self.figi = figiJSON["figi"]
 974            self.ticker = figiJSON["ticker"]
 975
 976            if requestPrice:
 977                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 978
 979                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
 980                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
 981
 982                else:
 983                    figiJSON["currentPrice"]["changes"] = 0
 984
 985            if show:
 986                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
 987
 988        else:
 989            if show:
 990                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
 991
 992        return figiJSON
 993
 994    def GetCurrentPrices(self, show: bool = True) -> dict:
 995        """
 996        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
 997        `{"buy": [{"price": 1243.8, "quantity": 193},
 998                  {"price": 1244.0, "quantity": 168},
 999                  {"price": 1244.8, "quantity": 5},
1000                  {"price": 1245.0, "quantity": 61},
1001                  {"price": 1245.4, "quantity": 60}],
1002          "sell": [{"price": 1243.6, "quantity": 8},
1003                   {"price": 1242.6, "quantity": 10},
1004                   {"price": 1242.4, "quantity": 18},
1005                   {"price": 1242.2, "quantity": 50},
1006                   {"price": 1242.0, "quantity": 113}],
1007          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1008        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1009        - sell: list of dicts with Buyers prices,
1010            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1011            - quantity: volume value by current price in lots,
1012        - limitUp: current trade session limit price, maximum,
1013        - limitDown: current trade session limit price, minimum,
1014        - lastPrice: last deal price of the instrument,
1015        - closePrice: previous trade session close price of the instrument.
1016
1017        See also: `SearchByTicker()` and `SearchByFIGI()`.
1018        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1019        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1020
1021        :param show: if `True` then print DOM to log and console.
1022        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1023                 If an error occurred then returns an empty record:
1024                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1025        """
1026        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1027
1028        if self.depth < 1:
1029            uLogger.error("Depth of Market (DOM) must be >=1!")
1030            raise Exception("Incorrect value")
1031
1032        if not (self.ticker or self.figi):
1033            uLogger.error("self.ticker or self.figi variables must be defined!")
1034            raise Exception("Ticker or FIGI required")
1035
1036        if self.ticker and not self.figi:
1037            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1038            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1039
1040        if not self.ticker and self.figi:
1041            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1042            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1043
1044        if not self.figi:
1045            uLogger.error("FIGI is not defined!")
1046            raise Exception("Ticker or FIGI required")
1047
1048        else:
1049            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1050
1051            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1052            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1053            self.body = str({"figi": self.figi, "depth": self.depth})
1054            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1055
1056            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1057                # list of dicts with sellers orders:
1058                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1059
1060                # list of dicts with buyers orders:
1061                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1062
1063                # max price of instrument at this time:
1064                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1065
1066                # min price of instrument at this time:
1067                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1068
1069                # last price of deal with instrument:
1070                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1071
1072                # last close price of instrument:
1073                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1074
1075            else:
1076                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1077                uLogger.debug("Server response: {}".format(pricesResponse))
1078
1079            if show:
1080                if prices["buy"] or prices["sell"]:
1081                    info = [
1082                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1083                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1084                            self.ticker,
1085                            self.figi,
1086                            self.depth,
1087                        ),
1088                        "-" * 60, "\n",
1089                        "             Orders of Buyers | Orders of Sellers\n",
1090                        "-" * 60, "\n",
1091                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1092                        "-" * 60, "\n",
1093                    ]
1094
1095                    if not prices["buy"]:
1096                        info.append("                              | No orders!\n")
1097                        sumBuy = 0
1098
1099                    else:
1100                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1101                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1102                        for item in maxMinSorted:
1103                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1104
1105                    if not prices["sell"]:
1106                        info.append("No orders!                    |\n")
1107                        sumSell = 0
1108
1109                    else:
1110                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1111                        for item in prices["sell"]:
1112                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1113
1114                    info.extend([
1115                        "-" * 60, "\n",
1116                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1117                        "-" * 60, "\n",
1118                    ])
1119
1120                    infoText = "".join(info)
1121
1122                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1123
1124                else:
1125                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1126
1127        return prices
1128
1129    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1130        """
1131        This method get and show information about all available broker instruments for current user account.
1132        If `instrumentsFile` string is not empty then also save information to this file.
1133
1134        :param show: if `True` then print results to console, if `False` — print only to file.
1135        :return: multi-lines string with all available broker instruments
1136        """
1137        if not self.iList:
1138            self.iList = self.Listing()
1139
1140        info = [
1141            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1142            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1143        ]
1144
1145        # add instruments count by type:
1146        for iType in self.iList.keys():
1147            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1148
1149        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1150        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1151
1152        # generating info tables with all instruments by type:
1153        for iType in self.iList.keys():
1154            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1155
1156            for instrument in self.iList[iType].keys():
1157                iName = self.iList[iType][instrument]["name"]  # instrument's name
1158                if len(iName) > 57:
1159                    iName = "{}...".format(iName[:54])  # right trim for a long string
1160
1161                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1162                    self.iList[iType][instrument]["ticker"],
1163                    iName,
1164                    self.iList[iType][instrument]["figi"],
1165                    self.iList[iType][instrument]["currency"],
1166                    self.iList[iType][instrument]["lot"],
1167                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1168                ))
1169
1170        infoText = "".join(info)
1171
1172        if show:
1173            uLogger.info(infoText)
1174
1175        if self.instrumentsFile:
1176            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1177                fH.write(infoText)
1178
1179            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1180
1181        return infoText
1182
1183    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1184        """
1185        This method search and show information about instruments by part of its ticker, FIGI or name.
1186        If `searchResultsFile` string is not empty then also save information to this file.
1187
1188        :param pattern: string with part of ticker, FIGI or instrument's name.
1189        :param show: if `True` then print results to console, if `False` — return list of result only.
1190        :return: list of dictionaries with all found instruments.
1191        """
1192        if not self.iList:
1193            self.iList = self.Listing()
1194
1195        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1196        compiledPattern = re.compile(pattern, re.IGNORECASE)
1197
1198        for iType in self.iList:
1199            for instrument in self.iList[iType].values():
1200                searchResult = compiledPattern.search(" ".join(
1201                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1202                ))
1203
1204                if searchResult:
1205                    searchResults[iType][instrument["ticker"]] = instrument
1206
1207        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1208        info = [
1209            "# Search results\n\n",
1210            "* **Search pattern:** [{}]\n".format(pattern),
1211            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1212            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1213        ]
1214        infoShort = info[:]
1215
1216        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1217        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1218        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1219
1220        if resultsLen == 0:
1221            info.append("\nNo results\n")
1222            infoShort.append("\nNo results\n")
1223            uLogger.warning("No results. Try changing your search pattern.")
1224
1225        else:
1226            for iType in searchResults:
1227                iTypeValuesCount = len(searchResults[iType].values())
1228                if iTypeValuesCount > 0:
1229                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1230                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1231
1232                    for instrument in searchResults[iType].values():
1233                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1234                            instrument["type"],
1235                            instrument["ticker"],
1236                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1237                            instrument["figi"],
1238                        ))
1239
1240                    if iTypeValuesCount <= 5:
1241                        infoShort.extend(info[-iTypeValuesCount:])
1242
1243                    else:
1244                        infoShort.extend(info[-5:])
1245                        infoShort.append(skippedLine)
1246
1247        infoText = "".join(info)
1248        infoTextShort = "".join(infoShort)
1249
1250        if show:
1251            uLogger.info(infoTextShort)
1252            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1253
1254        if self.searchResultsFile:
1255            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1256                fH.write(infoText)
1257
1258            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1259
1260        return searchResults
1261
1262    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1263        """
1264        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1265
1266        :param instruments: list of strings with tickers or FIGIs.
1267        :return: list with unique instrument FIGIs only.
1268        """
1269        requestedInstruments = []
1270        for iName in instruments:
1271            if iName not in self.aliases.keys():
1272                if iName not in requestedInstruments:
1273                    requestedInstruments.append(iName)
1274
1275            else:
1276                if iName not in requestedInstruments:
1277                    if self.aliases[iName] not in requestedInstruments:
1278                        requestedInstruments.append(self.aliases[iName])
1279
1280        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1281
1282        onlyUniqueFIGIs = []
1283        for iName in requestedInstruments:
1284            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1285                continue
1286
1287            self.ticker = iName
1288            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1289
1290            if not iData:
1291                self.ticker = ""
1292                self.figi = iName
1293
1294                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1295
1296                if not iData:
1297                    self.figi = ""
1298                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1299
1300            if iData and iData["figi"] not in onlyUniqueFIGIs:
1301                onlyUniqueFIGIs.append(iData["figi"])
1302
1303        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1304
1305        return onlyUniqueFIGIs
1306
1307    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1308        """
1309        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1310
1311        See limits: https://tinkoff.github.io/investAPI/limits/
1312
1313        If `pricesFile` string is not empty then also save information to this file.
1314
1315        :param instruments: list of strings with tickers or FIGIs.
1316        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1317        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1318                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1319        """
1320        if instruments is None or not instruments:
1321            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1322            raise Exception("Ticker or FIGI required")
1323
1324        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1325
1326        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1327
1328        iList = []  # trying to get info and current prices about all unique instruments:
1329        for self.figi in onlyUniqueFIGIs:
1330            iData = self.SearchByFIGI(requestPrice=True)
1331            iList.append(iData)
1332
1333        self.ShowListOfPrices(iList, show)
1334
1335        return iList
1336
1337    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1338        """
1339        Show table contains current prices of given instruments.
1340
1341        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1342                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1343        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1344        :return: multilines text in Markdown format as a table contains current prices.
1345        """
1346        infoText = ""
1347
1348        if show or self.pricesFile:
1349            info = [
1350                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1351                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1352                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1353            ]
1354
1355            for item in iList:
1356                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1357                    item["ticker"],
1358                    item["figi"],
1359                    item["type"],
1360                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1361                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1362                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1363                    "{} / {}".format(
1364                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1365                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1366                    ),
1367                    "{} / {}".format(
1368                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1369                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1370                    ),
1371                    item["currency"],
1372                ))
1373
1374            infoText = "".join(info)
1375
1376            if show:
1377                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1378
1379            if self.pricesFile:
1380                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1381                    fH.write(infoText)
1382
1383                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1384
1385        return infoText
1386
1387    def RequestTradingStatus(self) -> dict:
1388        """
1389        Requesting trading status for the instrument defined by `figi` variable.
1390
1391        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1392
1393        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1394
1395        :return: dictionary with trading status attributes. Response example:
1396                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1397                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1398        """
1399        if self.figi is None or not self.figi:
1400            uLogger.error("Variable `figi` must be defined for using this method!")
1401            raise Exception("FIGI required")
1402
1403        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1404
1405        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1406        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1407        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1408
1409        if self.moreDebug:
1410            uLogger.debug("Records about current trading status successfully received")
1411
1412        return tradingStatus
1413
1414    def RequestPortfolio(self) -> dict:
1415        """
1416        Requesting actual user's portfolio for current `accountId`.
1417
1418        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1419
1420        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1421
1422        :return: dictionary with user's portfolio.
1423        """
1424        if self.accountId is None or not self.accountId:
1425            uLogger.error("Variable `accountId` must be defined for using this method!")
1426            raise Exception("Account ID required")
1427
1428        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1429
1430        self.body = str({"accountId": self.accountId})
1431        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1432        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1433
1434        if self.moreDebug:
1435            uLogger.debug("Records about user's portfolio successfully received")
1436
1437        return rawPortfolio
1438
1439    def RequestPositions(self) -> dict:
1440        """
1441        Requesting open positions by currencies and instruments for current `accountId`.
1442
1443        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1444
1445        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1446
1447        :return: dictionary with open positions by instruments.
1448        """
1449        if self.accountId is None or not self.accountId:
1450            uLogger.error("Variable `accountId` must be defined for using this method!")
1451            raise Exception("Account ID required")
1452
1453        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1454
1455        self.body = str({"accountId": self.accountId})
1456        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1457        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1458
1459        if self.moreDebug:
1460            uLogger.debug("Records about current open positions successfully received")
1461
1462        return rawPositions
1463
1464    def RequestPendingOrders(self) -> list:
1465        """
1466        Requesting current actual pending orders for current `accountId`.
1467
1468        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1469
1470        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1471
1472        :return: list of dictionaries with pending orders.
1473        """
1474        if self.accountId is None or not self.accountId:
1475            uLogger.error("Variable `accountId` must be defined for using this method!")
1476            raise Exception("Account ID required")
1477
1478        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1479
1480        self.body = str({"accountId": self.accountId})
1481        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1482        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1483
1484        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1485
1486        return rawOrders
1487
1488    def RequestStopOrders(self) -> list:
1489        """
1490        Requesting current actual stop orders for current `accountId`.
1491
1492        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1493
1494        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1495
1496        :return: list of dictionaries with stop orders.
1497        """
1498        if self.accountId is None or not self.accountId:
1499            uLogger.error("Variable `accountId` must be defined for using this method!")
1500            raise Exception("Account ID required")
1501
1502        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1503
1504        self.body = str({"accountId": self.accountId})
1505        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1506        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1507
1508        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1509
1510        return rawStopOrders
1511
1512    def Overview(self, show: bool = False, details: str = "full") -> dict:
1513        """
1514        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1515        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1516        and `overviewBondsCalendarFile` are defined then also save information to file.
1517
1518        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1519        many requests about the state of the portfolio, and then, based on the received data, a large number
1520        of calculation and statistics are collected.
1521
1522        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1523        :param details: how detailed should the information be?
1524        - `full` — shows full available information about portfolio status (by default),
1525        - `positions` — shows only open positions,
1526        - `orders` — shows only sections of open limits and stop orders.
1527        - `digest` — show a short digest of the portfolio status,
1528        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1529        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1530        :return: dictionary with client's raw portfolio and some statistics.
1531        """
1532        if self.accountId is None or not self.accountId:
1533            uLogger.error("Variable `accountId` must be defined for using this method!")
1534            raise Exception("Account ID required")
1535
1536        view = {
1537            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1538                "headers": {},  # list of dictionaries, response headers without "positions" section
1539                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1540                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1541                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1542                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1543                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1544                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1545                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1546                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1547                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1548            },
1549            "stat": {  # --- some statistics calculated using "raw" sections:
1550                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1551                "availableRUB": 0.,  # available rubles (without other currencies)
1552                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1553                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1554                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1555                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1556                "sharesCostRUB": 0.,  # costs of all shares in RUB
1557                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1558                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1559                "futuresCostRUB": 0.,  # costs of all futures in RUB
1560                "Currencies": [],  # list of dictionaries of all currencies statistics
1561                "Shares": [],  # list of dictionaries of all shares statistics
1562                "Bonds": [],  # list of dictionaries of all bonds statistics
1563                "Etfs": [],  # list of dictionaries of all etfs statistics
1564                "Futures": [],  # list of dictionaries of all futures statistics
1565                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1566                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1567                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1568                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1569                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1570            },
1571            "analytics": {  # --- some analytics of portfolio:
1572                "distrByAssets": {},  # portfolio distribution by assets
1573                "distrByCompanies": {},  # portfolio distribution by companies
1574                "distrBySectors": {},  # portfolio distribution by sectors
1575                "distrByCurrencies": {},  # portfolio distribution by currencies
1576                "distrByCountries": {},  # portfolio distribution by countries
1577                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1578            }
1579        }
1580
1581        details = details.lower()
1582        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1583        if details not in availableDetails:
1584            details = "full"
1585            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1586
1587        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1588
1589        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1590        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1591        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1592        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1593
1594        # save response headers without "positions" section:
1595        for key in portfolioResponse.keys():
1596            if key != "positions":
1597                view["raw"]["headers"][key] = portfolioResponse[key]
1598
1599            else:
1600                continue
1601
1602        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1603        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1604        for item in portfolioResponse["positions"]:
1605            if item["instrumentType"] == "currency":
1606                self.figi = item["figi"]
1607                curr = self.SearchByFIGI(requestPrice=False)
1608
1609                # current price of currency in RUB:
1610                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1611                    "name": curr["name"],
1612                    "currentPrice": NanoToFloat(
1613                        item["currentPrice"]["units"],
1614                        item["currentPrice"]["nano"]
1615                    ),
1616                }
1617
1618                view["raw"]["Currencies"].append(item)
1619
1620            elif item["instrumentType"] == "share":
1621                view["raw"]["Shares"].append(item)
1622
1623            elif item["instrumentType"] == "bond":
1624                view["raw"]["Bonds"].append(item)
1625
1626            elif item["instrumentType"] == "etf":
1627                view["raw"]["Etfs"].append(item)
1628
1629            elif item["instrumentType"] == "futures":
1630                view["raw"]["Futures"].append(item)
1631
1632            else:
1633                continue
1634
1635        # how many volume of currencies (by ISO currency name) are blocked:
1636        for item in view["raw"]["positions"]["blocked"]:
1637            blocked = NanoToFloat(item["units"], item["nano"])
1638            if blocked > 0:
1639                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1640
1641        # how many volume of instruments (by FIGI) are blocked:
1642        for item in view["raw"]["positions"]["securities"]:
1643            blocked = int(item["blocked"])
1644            if blocked > 0:
1645                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1646
1647        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1648
1649        if "rub" in allBlocked.keys():
1650            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1651
1652        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1653        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1654        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1655        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1656        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1657        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1658        view["stat"]["portfolioCostRUB"] = sum([
1659            view["stat"]["allCurrenciesCostRUB"],
1660            view["stat"]["sharesCostRUB"],
1661            view["stat"]["bondsCostRUB"],
1662            view["stat"]["etfsCostRUB"],
1663            view["stat"]["futuresCostRUB"],
1664        ])
1665
1666        # --- calculating some portfolio statistics:
1667        byComp = {}  # distribution by companies
1668        bySect = {}  # distribution by sectors
1669        byCurr = {}  # distribution by currencies (include RUB)
1670        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1671        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1672
1673        for item in portfolioResponse["positions"]:
1674            self.figi = item["figi"]
1675            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1676
1677            if instrument:
1678                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1679                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1680
1681                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1682                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1683
1684                else:
1685                    blocked = 0
1686
1687                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1688                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1689                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1690                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1691                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1692                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1693                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1694                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1695                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1696                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1697                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1698                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1699
1700                statData = {
1701                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1702                    "ticker": instrument["ticker"],  # ticker by FIGI
1703                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1704                    "volume": volume,  # available volume of instrument
1705                    "lots": lots,  # volume in lots of instrument
1706                    "direction": direction,  # direction of an instrument's position: short or long
1707                    "blocked": blocked,  # blocked volume of currency or instrument
1708                    "currentPrice": curPrice,  # current instrument's price in basic asset
1709                    "average": average,  # current average position price
1710                    "cost": cost,  # current cost of all volume of instrument in basic asset
1711                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1712                    "costRUB": costRUB,  # cost of instrument in ruble
1713                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1714                    "profit": profit,  # expected profit at current moment
1715                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1716                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1717                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1718                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1719                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1720                    "step": instrument["step"],  # minimum price increment
1721                }
1722
1723                # adding distribution by unique countries:
1724                if statData["country"] not in byCountry.keys():
1725                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1726
1727                else:
1728                    byCountry[statData["country"]]["cost"] += costRUB
1729                    byCountry[statData["country"]]["percent"] += percentCostRUB
1730
1731                if item["instrumentType"] != "currency":
1732                    # adding distribution by unique companies:
1733                    if statData["name"]:
1734                        if statData["name"] not in byComp.keys():
1735                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1736
1737                        else:
1738                            byComp[statData["name"]]["cost"] += costRUB
1739                            byComp[statData["name"]]["percent"] += percentCostRUB
1740
1741                    # adding distribution by unique sectors:
1742                    if statData["sector"] not in bySect.keys():
1743                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1744
1745                    else:
1746                        bySect[statData["sector"]]["cost"] += costRUB
1747                        bySect[statData["sector"]]["percent"] += percentCostRUB
1748
1749                # adding distribution by unique currencies:
1750                if currency not in byCurr.keys():
1751                    byCurr[currency] = {
1752                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1753                        "cost": costRUB,
1754                        "percent": percentCostRUB
1755                    }
1756
1757                else:
1758                    byCurr[currency]["cost"] += costRUB
1759                    byCurr[currency]["percent"] += percentCostRUB
1760
1761                # saving statistics for every instrument:
1762                if item["instrumentType"] == "currency":
1763                    view["stat"]["Currencies"].append(statData)
1764
1765                    # update dict with free funds for trading (total - blocked) by currencies
1766                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1767                    view["stat"]["funds"][currency] = {
1768                        "total": volume,
1769                        "totalCostRUB": costRUB,  # total volume cost in rubles
1770                        "free": volume - blocked,
1771                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1772                    }
1773
1774                elif item["instrumentType"] == "share":
1775                    view["stat"]["Shares"].append(statData)
1776
1777                elif item["instrumentType"] == "bond":
1778                    view["stat"]["Bonds"].append(statData)
1779
1780                elif item["instrumentType"] == "etf":
1781                    view["stat"]["Etfs"].append(statData)
1782
1783                elif item["instrumentType"] == "Futures":
1784                    view["stat"]["Futures"].append(statData)
1785
1786                else:
1787                    continue
1788
1789        # total changes in Russian Ruble:
1790        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1791        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1792        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1793        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1794        view["stat"]["funds"]["rub"] = {
1795            "total": view["stat"]["availableRUB"],
1796            "totalCostRUB": view["stat"]["availableRUB"],
1797            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1798            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1799        }
1800
1801        # --- pending orders sector data:
1802        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1803        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1804
1805        for item in view["raw"]["orders"]:
1806            self.figi = item["figi"]
1807
1808            if item["figi"] not in uniquePendingOrdersFIGIs:
1809                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1810
1811                uniquePendingOrdersFIGIs.append(item["figi"])
1812                uniquePendingOrders[item["figi"]] = instrument
1813
1814            else:
1815                instrument = uniquePendingOrders[item["figi"]]
1816
1817            if instrument:
1818                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1819                orderType = TKS_ORDER_TYPES[item["orderType"]]
1820                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1821                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1822
1823                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1824                if item["direction"] == "ORDER_DIRECTION_BUY":
1825                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1826
1827                else:
1828                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1829
1830                # requested price for order execution:
1831                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1832
1833                # necessary changes in percent to reach target from current price:
1834                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1835
1836                view["stat"]["orders"].append({
1837                    "orderID": item["orderId"],  # orderId number parameter of current order
1838                    "figi": item["figi"],  # FIGI identification
1839                    "ticker": instrument["ticker"],  # ticker name by FIGI
1840                    "lotsRequested": item["lotsRequested"],  # requested lots value
1841                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1842                    "currentPrice": lastPrice,  # current instrument's price for defined action
1843                    "targetPrice": target,  # requested price for order execution in base currency
1844                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1845                    "percentChanges": changes,  # changes in percent to target from current price
1846                    "currency": item["currency"],  # instrument's currency name
1847                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1848                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1849                    "status": orderState,  # order status from TKS_ORDER_STATES
1850                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1851                })
1852
1853        # --- stop orders sector data:
1854        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1855        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1856
1857        for item in view["raw"]["stopOrders"]:
1858            self.figi = item["figi"]
1859
1860            if item["figi"] not in uniqueStopOrdersFIGIs:
1861                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1862
1863                uniqueStopOrdersFIGIs.append(item["figi"])
1864                uniqueStopOrders[item["figi"]] = instrument
1865
1866            else:
1867                instrument = uniqueStopOrders[item["figi"]]
1868
1869            if instrument:
1870                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1871                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1872                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1873
1874                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1875                if "expirationTime" in item.keys():
1876                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1877                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1878
1879                else:
1880                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1881                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1882
1883                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1884                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1885                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1886
1887                else:
1888                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1889
1890                # requested price when stop-order executed:
1891                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1892
1893                # price for limit-order, set up when stop-order executed:
1894                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1895
1896                # necessary changes in percent to reach target from current price:
1897                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1898
1899                view["stat"]["stopOrders"].append({
1900                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1901                    "figi": item["figi"],  # FIGI identification
1902                    "ticker": instrument["ticker"],  # ticker name by FIGI
1903                    "lotsRequested": item["lotsRequested"],  # requested lots value
1904                    "currentPrice": lastPrice,  # current instrument's price for defined action
1905                    "targetPrice": target,  # requested price for stop-order execution in base currency
1906                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1907                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1908                    "percentChanges": changes,  # changes in percent to target from current price
1909                    "currency": item["currency"],  # instrument's currency name
1910                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1911                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1912                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1913                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1914                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1915                })
1916
1917        # --- calculating data for analytics section:
1918        # portfolio distribution by assets:
1919        view["analytics"]["distrByAssets"] = {
1920            "Ruble": {
1921                "uniques": 1,
1922                "cost": view["stat"]["availableRUB"],
1923                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1924            },
1925            "Currencies": {
1926                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1927                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1928                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1929            },
1930            "Shares": {
1931                "uniques": len(view["stat"]["Shares"]),
1932                "cost": view["stat"]["sharesCostRUB"],
1933                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1934            },
1935            "Bonds": {
1936                "uniques": len(view["stat"]["Bonds"]),
1937                "cost": view["stat"]["bondsCostRUB"],
1938                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1939            },
1940            "Etfs": {
1941                "uniques": len(view["stat"]["Etfs"]),
1942                "cost": view["stat"]["etfsCostRUB"],
1943                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1944            },
1945            "Futures": {
1946                "uniques": len(view["stat"]["Futures"]),
1947                "cost": view["stat"]["futuresCostRUB"],
1948                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1949            },
1950        }
1951
1952        # portfolio distribution by companies:
1953        view["analytics"]["distrByCompanies"]["All money cash"] = {
1954            "ticker": "",
1955            "cost": view["stat"]["allCurrenciesCostRUB"],
1956            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1957        }
1958        view["analytics"]["distrByCompanies"].update(byComp)
1959
1960        # portfolio distribution by sectors:
1961        view["analytics"]["distrBySectors"]["All money cash"] = {
1962            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1963            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1964        }
1965        view["analytics"]["distrBySectors"].update(bySect)
1966
1967        # portfolio distribution by currencies:
1968        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1969            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1970
1971            if self.moreDebug:
1972                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1973
1974        view["analytics"]["distrByCurrencies"].update(byCurr)
1975        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1976        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1977
1978        # portfolio distribution by countries:
1979        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1980            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1981
1982            if self.moreDebug:
1983                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1984
1985        view["analytics"]["distrByCountries"].update(byCountry)
1986        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1987        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1988
1989        # --- Prepare text statistics overview in human-readable:
1990        if show:
1991            # Whatever the value `details`, header not changes:
1992            info = [
1993                "# Client's portfolio\n\n",
1994                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1995                "* **Account ID:** [{}]\n".format(self.accountId),
1996            ]
1997
1998            if details in ["full", "positions", "digest"]:
1999                info.extend([
2000                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2001                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2002                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2003                        view["stat"]["totalChangesRUB"],
2004                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2005                        view["stat"]["totalChangesPercentRUB"],
2006                    ),
2007                ])
2008
2009            if details in ["full", "positions"]:
2010                info.extend([
2011                    "## Open positions\n\n",
2012                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2013                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2014                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2015                        "{:.2f} ({:.2f}) rub".format(
2016                            view["stat"]["availableRUB"],
2017                            view["stat"]["blockedRUB"],
2018                        )
2019                    )
2020                ])
2021
2022                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2023                    return [
2024                        "|                             |                                 |          |              |              |                     |                              |\n",
2025                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2026                            noTradeStr if noTradeStr else typeStr,
2027                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2028                        ),
2029                    ]
2030
2031                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2032                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2033                        "{} [{}]".format(data["ticker"], data["figi"]),
2034                        "{:.2f} ({:.2f}) {}".format(
2035                            data["volume"],
2036                            data["blocked"],
2037                            data["currency"],
2038                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2039                            data["volume"],
2040                            data["blocked"],
2041                        ),
2042                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2043                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2044                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2045                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2046                        "{}{:.2f} {} ({}{:.2f}%)".format(
2047                            "+" if data["profit"] > 0 else "",
2048                            data["profit"], data["baseCurrencyName"],
2049                            "+" if data["percentProfit"] > 0 else "",
2050                            data["percentProfit"],
2051                        ),
2052                    )
2053
2054                # --- Show currencies section:
2055                if view["stat"]["Currencies"]:
2056                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2057                    for item in view["stat"]["Currencies"]:
2058                        info.append(_InfoStr(item, showCurrencyName=True))
2059
2060                else:
2061                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2062
2063                # --- Show shares section:
2064                if view["stat"]["Shares"]:
2065                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2066
2067                    for item in view["stat"]["Shares"]:
2068                        info.append(_InfoStr(item))
2069
2070                else:
2071                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2072
2073                # --- Show bonds section:
2074                if view["stat"]["Bonds"]:
2075                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2076
2077                    for item in view["stat"]["Bonds"]:
2078                        info.append(_InfoStr(item))
2079
2080                else:
2081                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2082
2083                # --- Show etfs section:
2084                if view["stat"]["Etfs"]:
2085                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2086
2087                    for item in view["stat"]["Etfs"]:
2088                        info.append(_InfoStr(item))
2089
2090                else:
2091                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2092
2093                # --- Show futures section:
2094                if view["stat"]["Futures"]:
2095                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2096
2097                    for item in view["stat"]["Futures"]:
2098                        info.append(_InfoStr(item))
2099
2100                else:
2101                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2102
2103            if details in ["full", "orders"]:
2104                # --- Show pending orders section:
2105                if view["stat"]["orders"]:
2106                    info.extend([
2107                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2108                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2109                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2110                    ])
2111
2112                    for item in view["stat"]["orders"]:
2113                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2114                            "{} [{}]".format(item["ticker"], item["figi"]),
2115                            item["orderID"],
2116                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2117                            "{} {} ({}{:.2f}%)".format(
2118                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2119                                item["baseCurrencyName"],
2120                                "+" if item["percentChanges"] > 0 else "",
2121                                float(item["percentChanges"]),
2122                            ),
2123                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2124                            item["action"],
2125                            item["type"],
2126                            item["date"],
2127                        ))
2128
2129                else:
2130                    info.append("\n## Total pending limit-orders: 0\n")
2131
2132                # --- Show stop orders section:
2133                if view["stat"]["stopOrders"]:
2134                    info.extend([
2135                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2136                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2137                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2138                    ])
2139
2140                    for item in view["stat"]["stopOrders"]:
2141                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2142                            "{} [{}]".format(item["ticker"], item["figi"]),
2143                            item["orderID"],
2144                            item["lotsRequested"],
2145                            "{} {} ({}{:.2f}%)".format(
2146                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2147                                item["baseCurrencyName"],
2148                                "+" if item["percentChanges"] > 0 else "",
2149                                float(item["percentChanges"]),
2150                            ),
2151                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2152                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2153                            item["action"],
2154                            item["type"],
2155                            item["expType"],
2156                            item["createDate"],
2157                            item["expDate"],
2158                        ))
2159
2160                else:
2161                    info.append("\n## Total stop-orders: 0\n")
2162
2163            if details in ["full", "analytics"]:
2164                # -- Show analytics section:
2165                if view["stat"]["portfolioCostRUB"] > 0:
2166                    info.extend([
2167                        "\n# Analytics\n"
2168                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2169                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2170                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2171                            view["stat"]["totalChangesRUB"],
2172                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2173                            view["stat"]["totalChangesPercentRUB"],
2174                        ),
2175                        "\n## Portfolio distribution by assets\n"
2176                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2177                        "|------------------------------------|---------|---------|--------------------|\n",
2178                    ])
2179
2180                    for key in view["analytics"]["distrByAssets"].keys():
2181                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2182                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2183                                key,
2184                                view["analytics"]["distrByAssets"][key]["uniques"],
2185                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2186                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2187                            ))
2188
2189                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2190
2191                    info.extend([
2192                        "\n## Portfolio distribution by companies\n"
2193                        "\n| Company                                      | Percent | Current cost       |\n",
2194                        aSepLine,
2195                    ])
2196
2197                    for company in view["analytics"]["distrByCompanies"].keys():
2198                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2199                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2200                                "{}{}".format(
2201                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2202                                    company,
2203                                ),
2204                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2205                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2206                            ))
2207
2208                    info.extend([
2209                        "\n## Portfolio distribution by sectors\n"
2210                        "\n| Sector                                       | Percent | Current cost       |\n",
2211                        aSepLine,
2212                    ])
2213
2214                    for sector in view["analytics"]["distrBySectors"].keys():
2215                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2216                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2217                                sector,
2218                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2219                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2220                            ))
2221
2222                    info.extend([
2223                        "\n## Portfolio distribution by currencies\n"
2224                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2225                        aSepLine,
2226                    ])
2227
2228                    for curr in view["analytics"]["distrByCurrencies"].keys():
2229                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2230                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2231                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2232                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2233                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2234                            ))
2235
2236                    info.extend([
2237                        "\n## Portfolio distribution by countries\n"
2238                        "\n| Assets by country                            | Percent | Current cost       |\n",
2239                        aSepLine,
2240                    ])
2241
2242                    for country in view["analytics"]["distrByCountries"].keys():
2243                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2244                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2245                                country,
2246                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2247                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2248                            ))
2249
2250            if details in ["full", "calendar"]:
2251                # -- Show bonds payment calendar section:
2252                if view["stat"]["Bonds"]:
2253                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2254                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2255                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2256
2257                else:
2258                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2259
2260            infoText = "".join(info)
2261
2262            uLogger.info(infoText)
2263
2264            if details == "full" and self.overviewFile:
2265                filename = self.overviewFile
2266
2267            elif details == "digest" and self.overviewDigestFile:
2268                filename = self.overviewDigestFile
2269
2270            elif details == "positions" and self.overviewPositionsFile:
2271                filename = self.overviewPositionsFile
2272
2273            elif details == "orders" and self.overviewOrdersFile:
2274                filename = self.overviewOrdersFile
2275
2276            elif details == "analytics" and self.overviewAnalyticsFile:
2277                filename = self.overviewAnalyticsFile
2278
2279            elif details == "calendar" and self.overviewBondsCalendarFile:
2280                filename = self.overviewBondsCalendarFile
2281
2282            else:
2283                filename = ""
2284
2285            if filename:
2286                with open(filename, "w", encoding="UTF-8") as fH:
2287                    fH.write(infoText)
2288
2289                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2290
2291        return view
2292
2293    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2294        """
2295        Returns history operations between two given dates for current `accountId`.
2296        If `reportFile` string is not empty then also save human-readable report.
2297        Shows some statistical data of closed positions.
2298
2299        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2300        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2301        :param show: if `True` then also prints all records to the console.
2302        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2303        :return: original list of dictionaries with history of deals records from API ("operations" key):
2304                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2305                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2306        """
2307        if self.accountId is None or not self.accountId:
2308            uLogger.error("Variable `accountId` must be defined for using this method!")
2309            raise Exception("Account ID required")
2310
2311        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2312
2313        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2314
2315        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2316        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2317        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2318        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2319        customStat = {}  # custom statistics in additional to responseJSON
2320
2321        # --- output report in human-readable format:
2322        if show or self.reportFile:
2323            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2324            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2325            nextDay = ""
2326
2327            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2328
2329            if len(ops) > 0:
2330                customStat = {
2331                    "opsCount": 0,  # total operations count
2332                    "buyCount": 0,  # buy operations
2333                    "sellCount": 0,  # sell operations
2334                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2335                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2336                    "payIn": {"rub": 0.},  # Deposit brokerage account
2337                    "payOut": {"rub": 0.},  # Withdrawals
2338                    "divs": {"rub": 0.},  # Dividends income
2339                    "coupons": {"rub": 0.},  # Coupon's income
2340                    "brokerCom": {"rub": 0.},  # Service commissions
2341                    "serviceCom": {"rub": 0.},  # Service commissions
2342                    "marginCom": {"rub": 0.},  # Margin commissions
2343                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2344                }
2345
2346                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2347                for item in ops:
2348                    if item["state"] == "OPERATION_STATE_EXECUTED":
2349                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2350
2351                        # count buy operations:
2352                        if "_BUY" in item["operationType"]:
2353                            customStat["buyCount"] += 1
2354
2355                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2356                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2357
2358                            else:
2359                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2360
2361                        # count sell operations:
2362                        elif "_SELL" in item["operationType"]:
2363                            customStat["sellCount"] += 1
2364
2365                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2366                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2367
2368                            else:
2369                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2370
2371                        # count incoming operations:
2372                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2373                            if item["payment"]["currency"] in customStat["payIn"].keys():
2374                                customStat["payIn"][item["payment"]["currency"]] += payment
2375
2376                            else:
2377                                customStat["payIn"][item["payment"]["currency"]] = payment
2378
2379                        # count withdrawals operations:
2380                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2381                            if item["payment"]["currency"] in customStat["payOut"].keys():
2382                                customStat["payOut"][item["payment"]["currency"]] += payment
2383
2384                            else:
2385                                customStat["payOut"][item["payment"]["currency"]] = payment
2386
2387                        # count dividends income:
2388                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2389                            if item["payment"]["currency"] in customStat["divs"].keys():
2390                                customStat["divs"][item["payment"]["currency"]] += payment
2391
2392                            else:
2393                                customStat["divs"][item["payment"]["currency"]] = payment
2394
2395                        # count coupon's income:
2396                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2397                            if item["payment"]["currency"] in customStat["coupons"].keys():
2398                                customStat["coupons"][item["payment"]["currency"]] += payment
2399
2400                            else:
2401                                customStat["coupons"][item["payment"]["currency"]] = payment
2402
2403                        # count broker commissions:
2404                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2405                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2406                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2407
2408                            else:
2409                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2410
2411                        # count service commissions:
2412                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2413                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2414                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2415
2416                            else:
2417                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2418
2419                        # count margin commissions:
2420                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2421                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2422                                customStat["marginCom"][item["payment"]["currency"]] += payment
2423
2424                            else:
2425                                customStat["marginCom"][item["payment"]["currency"]] = payment
2426
2427                        # count withholding taxes:
2428                        elif "_TAX" in item["operationType"]:
2429                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2430                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2431
2432                            else:
2433                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2434
2435                        else:
2436                            continue
2437
2438                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2439
2440                # --- view "Actions" lines:
2441                info.extend([
2442                    "| Report sections            |                               |                              |                      |                        |\n",
2443                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2444                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2445                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2446                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2447                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2448                    ),
2449                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2450                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2451                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2452                    ),
2453                ])
2454
2455                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2456                for key in opsKeys:
2457                    if key == "rub":
2458                        continue
2459
2460                    info.extend([
2461                        "|                            |                               | {:<28} |                      |                        |\n".format(
2462                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2463                        ),
2464                        "|                            |                               | {:<28} |                      |                        |\n".format(
2465                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2466                        ),
2467                    ])
2468
2469                info.append(splitLine1)
2470
2471                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2472                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2473                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2474                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2475                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2476                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2477                    )
2478
2479                # --- view "Payments" lines:
2480                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2481                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2482
2483                for key in paymentsKeys:
2484                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2485
2486                info.append(splitLine1)
2487
2488                # --- view "Commissions and taxes" lines:
2489                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2490                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2491
2492                for key in comKeys:
2493                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2494
2495                info.append(splitLine1)
2496
2497                info.extend([
2498                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2499                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2500                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2501                ])
2502
2503            else:
2504                info.append("Broker returned no operations during this period\n")
2505
2506            # --- view "Operations" section:
2507            for item in ops:
2508                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2509                    continue
2510
2511                else:
2512                    self.figi = item["figi"] if item["figi"] else ""
2513                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2514                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2515
2516                    # group of deals during one day:
2517                    if nextDay and item["date"].split("T")[0] != nextDay:
2518                        info.append(splitLine2)
2519                        nextDay = ""
2520
2521                    else:
2522                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2523
2524                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2525                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2526                        self.figi if self.figi else "—",
2527                        instrument["ticker"] if instrument else "—",
2528                        instrument["type"] if instrument else "—",
2529                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2530                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2531                        TKS_OPERATION_STATES[item["state"]],
2532                        TKS_OPERATION_TYPES[item["operationType"]],
2533                    ))
2534
2535            infoText = "".join(info)
2536
2537            if show:
2538                if self.moreDebug:
2539                    uLogger.debug("Records about history of a client's operations successfully received")
2540
2541                uLogger.info(infoText)
2542
2543            if self.reportFile:
2544                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2545                    fH.write(infoText)
2546
2547                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2548
2549        return ops, customStat
2550
2551    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2552        """
2553        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2554
2555        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2556        Warning! Broker server used ISO UTC time by default.
2557
2558        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2559        Also, `historyFile` used to update history with `onlyMissing` parameter.
2560
2561        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2562
2563        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2564        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2565        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2566                         `"hour"`, `"day"`. Default: `"hour"`.
2567        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2568                            False by default. Warning! History appends only from last candle to current time
2569                            with always update last candle!
2570        :param csvSep: separator if csv-file is used, `,` by default.
2571        :param show: if `True` then also prints Pandas DataFrame to the console.
2572        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2573                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2574        """
2575        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2576        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2577        history = None  # empty pandas object for history
2578
2579        if interval not in TKS_CANDLE_INTERVALS.keys():
2580            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2581            raise Exception("Incorrect value")
2582
2583        if not (self.ticker or self.figi):
2584            uLogger.error("Ticker or FIGI must be defined!")
2585            raise Exception("Ticker or FIGI required")
2586
2587        if self.ticker and not self.figi:
2588            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2589            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2590
2591        if self.figi and not self.ticker:
2592            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2593            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2594
2595        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2596        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2597        if interval.lower() != "day":
2598            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2599
2600        delta = dtEnd - dtStart  # current UTC time minus last time in file
2601        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2602
2603        # calculate history length in candles:
2604        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2605        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2606            length += 1  # to avoid fraction time
2607
2608        # calculate data blocks count:
2609        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2610
2611        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2612        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2613        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2614        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2615        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2616
2617        tempOld = None  # pandas object for old history, if --only-missing key present
2618        lastTime = None  # datetime object of last old candle in file
2619
2620        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2621            uLogger.debug("--only-missing key present, add only last missing candles...")
2622            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2623
2624            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2625
2626            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2627            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2628            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2629            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2630
2631            # get last datetime object from last string in file or minus 1 delta if file is empty:
2632            if len(tempOld) > 0:
2633                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2634
2635            else:
2636                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2637
2638            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2639
2640        responseJSONs = []  # raw history blocks of data
2641
2642        blockEnd = dtEnd
2643        for item in range(blocks):
2644            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2645            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2646
2647            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2648                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2649            ))
2650
2651            if blockStart == blockEnd:
2652                uLogger.debug("Skipped this zero-length block...")
2653
2654            else:
2655                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2656                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2657                self.body = str({
2658                    "figi": self.figi,
2659                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2660                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2661                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2662                })
2663                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2664
2665                if "code" in responseJSON.keys():
2666                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2667
2668                else:
2669                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2670                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2671
2672                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2673
2674            blockEnd = blockStart
2675
2676        printCount = len(responseJSONs)  # candles to show in console
2677        if responseJSONs:
2678            tempHistory = pd.DataFrame(
2679                data={
2680                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2681                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2682                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2683                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2684                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2685                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2686                    "volume": [int(item["volume"]) for item in responseJSONs],
2687                },
2688                index=range(len(responseJSONs)),
2689                columns=["date", "time", "open", "high", "low", "close", "volume"],
2690            )
2691            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2692            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2693
2694            # append only newest candles to old history if --only-missing key present:
2695            if onlyMissing and tempOld is not None and lastTime is not None:
2696                index = 0  # find start index in tempHistory data:
2697
2698                for i, item in tempHistory.iterrows():
2699                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2700
2701                    if curTime == lastTime:
2702                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2703                        index = i
2704                        printCount = index + 1
2705                        break
2706
2707                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2708
2709            else:
2710                history = tempHistory  # if no `--only-missing` key then load full data from server
2711
2712            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2713
2714        if history is not None and not history.empty:
2715            if show:
2716                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2717                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2718                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2719                ))
2720
2721        else:
2722            uLogger.warning("Received an empty candles history!")
2723
2724        if self.historyFile is not None:
2725            if history is not None and not history.empty:
2726                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2727                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2728
2729            else:
2730                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2731
2732        else:
2733            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2734
2735        return history
2736
2737    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2738        """
2739        Load candles history from csv-file and return Pandas DataFrame object.
2740
2741        See also: `History()` and `ShowHistoryChart()` methods.
2742
2743        :param filePath: path to csv-file to open.
2744        """
2745        loadedHistory = None  # init candles data object
2746
2747        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2748
2749        if os.path.exists(filePath):
2750            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2751
2752            tfStr = self.priceModel.FormattedDelta(
2753                self.priceModel.timeframe,
2754                "{days} days {hours}h {minutes}m {seconds}s",
2755            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2756                self.priceModel.timeframe,
2757                "{hours}h {minutes}m {seconds}s",
2758            )
2759
2760            if loadedHistory is not None and not loadedHistory.empty:
2761                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2762                    len(loadedHistory),
2763                    tfStr,
2764                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2765                )
2766
2767            else:
2768                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2769
2770        else:
2771            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2772
2773        return loadedHistory
2774
2775    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2776        """
2777        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2778
2779        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2780        Default: `index.html` (both for interact and non-interact candlesticks chart).
2781
2782        See also: `History()` and `LoadHistory()` methods.
2783
2784        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2785        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2786                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2787                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2788                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2789        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2790                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2791        """
2792        if isinstance(candles, str):
2793            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2794            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2795
2796        elif isinstance(candles, pd.DataFrame):
2797            self.priceModel.prices = candles  # set candles chain from variable
2798            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2799
2800            if "datetime" not in candles.columns:
2801                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2802
2803        else:
2804            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2805            raise Exception("Incorrect value")
2806
2807        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2808
2809        if interact:
2810            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2811
2812            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2813
2814        else:
2815            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2816
2817            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2818
2819        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2820
2821    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2822        """
2823        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2824        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2825
2826        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2827
2828        :param operation: string "Buy" or "Sell".
2829        :param lots: volume, integer count of lots >= 1.
2830        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2831        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2832        :param expDate: string "Undefined" by default or local date in future,
2833                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2834        :return: JSON with response from broker server.
2835        """
2836        if self.accountId is None or not self.accountId:
2837            uLogger.error("Variable `accountId` must be defined for using this method!")
2838            raise Exception("Account ID required")
2839
2840        if operation is None or not operation or operation not in ("Buy", "Sell"):
2841            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2842            raise Exception("Incorrect value")
2843
2844        if lots is None or lots < 1:
2845            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2846            lots = 1
2847
2848        if tp is None or tp < 0:
2849            tp = 0
2850
2851        if sl is None or sl < 0:
2852            sl = 0
2853
2854        if expDate is None or not expDate:
2855            expDate = "Undefined"
2856
2857        if not (self.ticker or self.figi):
2858            uLogger.error("Ticker or FIGI must be defined!")
2859            raise Exception("Ticker or FIGI required")
2860
2861        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2862        self.ticker = instrument["ticker"]
2863        self.figi = instrument["figi"]
2864
2865        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2866
2867        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2868        self.body = str({
2869            "figi": self.figi,
2870            "quantity": str(lots),
2871            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2872            "accountId": str(self.accountId),
2873            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2874        })
2875        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2876
2877        if "orderId" in response.keys():
2878            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2879                operation, response["orderId"],
2880                self.ticker, self.figi, lots,
2881                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2882                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2883                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2884            ))
2885
2886            if tp > 0:
2887                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2888
2889            if sl > 0:
2890                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2891
2892        else:
2893            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2894
2895        return response
2896
2897    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2898        """
2899        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2900        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2901
2902        See also: `Order()` and `Trade()` docstrings.
2903
2904        :param lots: volume, integer count of lots >= 1.
2905        :param tp: float > 0, take profit price of stop-order.
2906        :param sl: float > 0, stop loss price of stop-order.
2907        :param expDate: it's a local date in future.
2908                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2909        :return: JSON with response from broker server.
2910        """
2911        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
2912
2913    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2914        """
2915        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2916        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2917
2918        See also: `Order()` and `Trade()` docstrings.
2919
2920        :param lots: volume, integer count of lots >= 1.
2921        :param tp: float > 0, take profit price of stop-order.
2922        :param sl: float > 0, stop loss price of stop-order.
2923        :param expDate: it's a local date in the future.
2924                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2925        :return: JSON with response from broker server.
2926        """
2927        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
2928
2929    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2930        """
2931        Close position of given instruments.
2932
2933        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2934        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2935                         This avoids unnecessary downloading data from the server.
2936        """
2937        if instruments is None or not instruments:
2938            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2939            raise Exception("Ticker or FIGI required")
2940
2941        if isinstance(instruments, str):
2942            instruments = [instruments]
2943
2944        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2945        if uniqueInstruments:
2946            if portfolio is None or not portfolio:
2947                portfolio = self.Overview(show=False)
2948
2949            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2950            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2951
2952            for self.figi in uniqueInstruments:
2953                if self.figi not in allOpened:
2954                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2955                    continue
2956
2957                # search open trade info about instrument by ticker:
2958                instrument = {}
2959                for iType in TKS_INSTRUMENTS:
2960                    if instrument:
2961                        break
2962
2963                    for item in portfolio["stat"][iType]:
2964                        if item["figi"] == self.figi:
2965                            instrument = item
2966                            break
2967
2968                if instrument:
2969                    self.ticker = instrument["ticker"]
2970                    self.figi = instrument["figi"]
2971
2972                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2973                        self.ticker,
2974                        self.figi,
2975                        int(instrument["volume"]),
2976                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2977                    ))
2978
2979                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2980
2981                    if tradeLots > 0:
2982                        if instrument["blocked"] > 0:
2983                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2984                                instrument["blocked"],
2985                                self.ticker,
2986                                tradeLots,
2987                            ))
2988
2989                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2990                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
2991
2992                    else:
2993                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
2994
2995    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2996        """
2997        Close all positions of given instruments with defined type.
2998
2999        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3000        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3001                         This avoids unnecessary downloading data from the server.
3002        """
3003        if iType not in TKS_INSTRUMENTS:
3004            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3005
3006        else:
3007            if portfolio is None or not portfolio:
3008                portfolio = self.Overview(show=False)
3009
3010            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3011            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3012
3013            if tickers and portfolio:
3014                self.CloseTrades(tickers, portfolio)
3015
3016            else:
3017                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3018
3019    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3020        """
3021        Universal method to create market or limit orders with all available parameters for current `accountId`.
3022        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3023
3024        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3025        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3026
3027        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3028        then broker immediately open market order as you can do simple --buy or --sell operations!
3029
3030        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3031        When current price will go up or down to target price value then broker opens a limit order.
3032        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3033
3034        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3035
3036        :param operation: string "Buy" or "Sell".
3037        :param orderType: string "Limit" or "Stop".
3038        :param lots: volume, integer count of lots >= 1.
3039        :param targetPrice: target price > 0. This is open trade price for limit order.
3040        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3041                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3042        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3043                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3044                         Stop loss order always executed by market price.
3045        :param expDate: string "Undefined" by default or local date in future.
3046                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3047                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3048                        A limit order has no expiration date, it lasts until the end of the trading day.
3049        :return: JSON with response from broker server.
3050        """
3051        if self.accountId is None or not self.accountId:
3052            uLogger.error("Variable `accountId` must be defined for using this method!")
3053            raise Exception("Account ID required")
3054
3055        if operation is None or not operation or operation not in ("Buy", "Sell"):
3056            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3057            raise Exception("Incorrect value")
3058
3059        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3060            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3061            raise Exception("Incorrect value")
3062
3063        if lots is None or lots < 1:
3064            uLogger.error("You must define trade volume > 0: integer count of lots!")
3065            raise Exception("Incorrect value")
3066
3067        if targetPrice is None or targetPrice <= 0:
3068            uLogger.error("Target price for limit-order must be greater than 0!")
3069            raise Exception("Incorrect value")
3070
3071        if limitPrice is None or limitPrice <= 0:
3072            limitPrice = targetPrice
3073
3074        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3075            stopType = "Limit"
3076
3077        if expDate is None or not expDate:
3078            expDate = "Undefined"
3079
3080        if not (self.ticker or self.figi):
3081            uLogger.error("Tocker or FIGI must be defined!")
3082            raise Exception("Ticker or FIGI required")
3083
3084        response = {}
3085        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3086        self.ticker = instrument["ticker"]
3087        self.figi = instrument["figi"]
3088
3089        if orderType == "Limit":
3090            uLogger.debug(
3091                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3092                    self.ticker, self.figi,
3093                    operation, lots, targetPrice, instrument["currency"],
3094                ))
3095
3096            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3097            self.body = str({
3098                "figi": self.figi,
3099                "quantity": str(lots),
3100                "price": FloatToNano(targetPrice),
3101                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3102                "accountId": str(self.accountId),
3103                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3104            })
3105            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3106
3107            if "orderId" in response.keys():
3108                uLogger.info(
3109                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3110                        response["orderId"],
3111                        self.ticker, self.figi,
3112                        operation, lots, targetPrice, instrument["currency"],
3113                    ))
3114
3115                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3116                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3117                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3118                            targetPrice, instrument["currency"],
3119                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3120                        ))
3121
3122                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3123                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3124                            targetPrice, instrument["currency"],
3125                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3126                        ))
3127
3128            else:
3129                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3130
3131        if orderType == "Stop":
3132            uLogger.debug(
3133                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3134                    self.ticker, self.figi,
3135                    operation, lots,
3136                    targetPrice, instrument["currency"],
3137                    limitPrice, instrument["currency"],
3138                    stopType, expDate,
3139                ))
3140
3141            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3142            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3143            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3144
3145            body = {
3146                "figi": self.figi,
3147                "quantity": str(lots),
3148                "price": FloatToNano(limitPrice),
3149                "stopPrice": FloatToNano(targetPrice),
3150                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3151                "accountId": str(self.accountId),
3152                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3153                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3154            }
3155
3156            if expDateUTC:
3157                body["expireDate"] = expDateUTC
3158
3159            self.body = str(body)
3160            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3161
3162            if "stopOrderId" in response.keys():
3163                uLogger.info(
3164                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3165                        response["stopOrderId"],
3166                        self.ticker, self.figi,
3167                        operation, lots,
3168                        targetPrice, instrument["currency"],
3169                        limitPrice, instrument["currency"],
3170                        TKS_STOP_ORDER_TYPES[stopOrderType],
3171                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3172                    ))
3173
3174                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3175                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3176                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3177                            targetPrice, instrument["currency"],
3178                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3179                        ))
3180
3181                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3182                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3183                            targetPrice, instrument["currency"],
3184                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3185                        ))
3186
3187            else:
3188                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3189
3190        return response
3191
3192    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3193        """
3194        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3195        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3196        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3197        See also: `Order()` docstring.
3198
3199        :param lots: volume, integer count of lots >= 1.
3200        :param targetPrice: target price > 0. This is open trade price for limit order.
3201        :return: JSON with response from broker server.
3202        """
3203        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3204
3205    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3206        """
3207        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3208        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3209        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3210        target price value then broker opens a limit order. See also: `Order()` docstring.
3211
3212        :param lots: volume, integer count of lots >= 1.
3213        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3214        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3215                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3216        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3217                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3218        :param expDate: string "Undefined" by default or local date in future.
3219                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3220                        This date is converting to UTC format for server.
3221        :return: JSON with response from broker server.
3222        """
3223        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3224
3225    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3226        """
3227        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3228        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3229        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3230        See also: `Order()` docstring.
3231
3232        :param lots: volume, integer count of lots >= 1.
3233        :param targetPrice: target price > 0. This is open trade price for limit order.
3234        :return: JSON with response from broker server.
3235        """
3236        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3237
3238    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3239        """
3240        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3241        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3242        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3243        target price value then broker opens a limit order. See also: `Order()` docstring.
3244
3245        :param lots: volume, integer count of lots >= 1.
3246        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3247        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3248                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3249        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3250                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3251        :param expDate: string "Undefined" by default or local date in future.
3252                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3253                        This date is converting to UTC format for server.
3254        :return: JSON with response from broker server.
3255        """
3256        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3257
3258    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3259        """
3260        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3261
3262        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3263        :param allOrdersIDs: pre-received lists of all active pending orders.
3264                             This avoids unnecessary downloading data from the server.
3265        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3266        """
3267        if self.accountId is None or not self.accountId:
3268            uLogger.error("Variable `accountId` must be defined for using this method!")
3269            raise Exception("Account ID required")
3270
3271        if orderIDs:
3272            if allOrdersIDs is None or not allOrdersIDs:
3273                rawOrders = self.RequestPendingOrders()
3274                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3275
3276            if allStopOrdersIDs is None or not allStopOrdersIDs:
3277                rawStopOrders = self.RequestStopOrders()
3278                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3279
3280            for orderID in orderIDs:
3281                idInPendingOrders = orderID in allOrdersIDs
3282                idInStopOrders = orderID in allStopOrdersIDs
3283
3284                if not (idInPendingOrders or idInStopOrders):
3285                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3286                    continue
3287
3288                else:
3289                    if idInPendingOrders:
3290                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3291
3292                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3293                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3294                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3295                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3296
3297                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3298                            if self.moreDebug:
3299                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3300
3301                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3302
3303                        else:
3304                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3305
3306                    elif idInStopOrders:
3307                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3308
3309                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3310                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3311                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3312                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3313
3314                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3315                            if self.moreDebug:
3316                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3317
3318                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3319
3320                        else:
3321                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3322
3323                    else:
3324                        continue
3325
3326    def CloseAllOrders(self) -> None:
3327        """
3328        Gets a list of open pending and stop orders and cancel it all.
3329        """
3330        rawOrders = self.RequestPendingOrders()
3331        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3332        lenOrders = len(allOrdersIDs)
3333
3334        rawStopOrders = self.RequestStopOrders()
3335        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3336        lenSOrders = len(allStopOrdersIDs)
3337
3338        if lenOrders > 0 or lenSOrders > 0:
3339            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3340
3341            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3342
3343        else:
3344            uLogger.info("Orders not found, nothing to cancel.")
3345
3346    def CloseAll(self, *args) -> None:
3347        """
3348        Close all available (not blocked) opened trades and orders.
3349
3350        Also, you can select one or more keywords case-insensitive:
3351        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3352
3353        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3354        """
3355        overview = self.Overview(show=False)  # get all open trades info
3356
3357        if len(args) == 0:
3358            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3359            self.CloseAllOrders()  # close all pending and stop orders
3360
3361            for iType in TKS_INSTRUMENTS:
3362                if iType != "Currencies":
3363                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3364
3365        else:
3366            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3367            lowerArgs = [x.lower() for x in args]
3368
3369            if "orders" in lowerArgs:
3370                self.CloseAllOrders()  # close all pending and stop orders
3371
3372            for iType in TKS_INSTRUMENTS:
3373                if iType.lower() in lowerArgs and iType != "Currencies":
3374                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3375
3376    @staticmethod
3377    def ParseOrderParameters(operation, **inputParameters):
3378        """
3379        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3380
3381        :param operation: string "Buy" or "Sell".
3382        :param inputParameters: this is dict of strings that looks like this
3383               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3384               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3385               "prices" key: one or more prices to open limit-orders
3386               Counts of values in lots and prices lists must be equals!
3387        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3388        """
3389        # TODO: update order grid work with api v2
3390        pass
3391        # uLogger.debug("Input parameters: {}".format(inputParameters))
3392        #
3393        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3394        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3395        #     raise Exception("Incorrect value")
3396        #
3397        # if "l" in inputParameters.keys():
3398        #     inputParameters["lots"] = inputParameters.pop("l")
3399        #
3400        # if "p" in inputParameters.keys():
3401        #     inputParameters["prices"] = inputParameters.pop("p")
3402        #
3403        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3404        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3405        #     raise Exception("Incorrect value")
3406        #
3407        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3408        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3409        #
3410        # if len(lots) != len(prices):
3411        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3412        #     raise Exception("Incorrect value")
3413        #
3414        # uLogger.debug("Extracted parameters for orders:")
3415        # uLogger.debug("lots = {}".format(lots))
3416        # uLogger.debug("prices = {}".format(prices))
3417        #
3418        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3419        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3420        # uLogger.debug("Order parameters: {}".format(result))
3421        #
3422        # return result
3423
3424    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3425        """
3426        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3427
3428        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3429        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3430        """
3431        result = False
3432        msg = "Instrument not defined!"
3433
3434        if portfolio is None or not portfolio:
3435            portfolio = self.Overview(show=False)
3436
3437        if self.ticker:
3438            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3439            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3440
3441            for iType in TKS_INSTRUMENTS:
3442                for instrument in portfolio["stat"][iType]:
3443                    if instrument["ticker"] == self.ticker:
3444                        result = True
3445                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3446                        break
3447
3448        elif self.figi:
3449            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3450            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3451
3452            for iType in TKS_INSTRUMENTS:
3453                for instrument in portfolio["stat"][iType]:
3454                    if instrument["figi"] == self.figi:
3455                        result = True
3456                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3457                        break
3458
3459        else:
3460            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3461
3462        uLogger.debug(msg)
3463
3464        return result
3465
3466    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3467        """
3468        Returns instrument from the user's portfolio if it presents there.
3469        Instrument must be defined by `ticker` (highly priority) or `figi`.
3470
3471        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3472        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3473        """
3474        result = None
3475        msg = "Instrument not defined!"
3476
3477        if portfolio is None or not portfolio:
3478            portfolio = self.Overview(show=False)
3479
3480        if self.ticker:
3481            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3482            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3483
3484            for iType in TKS_INSTRUMENTS:
3485                for instrument in portfolio["stat"][iType]:
3486                    if instrument["ticker"] == self.ticker:
3487                        result = instrument
3488                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3489                        break
3490
3491        elif self.figi:
3492            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3493            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3494
3495            for iType in TKS_INSTRUMENTS:
3496                for instrument in portfolio["stat"][iType]:
3497                    if instrument["figi"] == self.figi:
3498                        result = instrument
3499                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3500                        break
3501
3502        else:
3503            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3504
3505        uLogger.debug(msg)
3506
3507        return result
3508
3509    def RequestLimits(self) -> dict:
3510        """
3511        Method for obtaining the available funds for withdrawal for current `accountId`.
3512
3513        See also:
3514        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3515        - `OverviewLimits()` method
3516
3517        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3518                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3519                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3520                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3521        """
3522        if self.accountId is None or not self.accountId:
3523            uLogger.error("Variable `accountId` must be defined for using this method!")
3524            raise Exception("Account ID required")
3525
3526        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3527
3528        self.body = str({"accountId": self.accountId})
3529        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3530        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3531
3532        if self.moreDebug:
3533            uLogger.debug("Records about available funds for withdrawal successfully received")
3534
3535        return rawLimits
3536
3537    def OverviewLimits(self, show: bool = False) -> dict:
3538        """
3539        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3540
3541        See also: `RequestLimits()`.
3542
3543        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3544        :return: dict with raw parsed data from server and some calculated statistics about it.
3545        """
3546        if self.accountId is None or not self.accountId:
3547            uLogger.error("Variable `accountId` must be defined for using this method!")
3548            raise Exception("Account ID required")
3549
3550        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3551
3552        view = {
3553            "rawLimits": rawLimits,
3554            "limits": {  # parsed data for every currency:
3555                "money": {  # this is an array of portfolio currency positions
3556                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3557                },
3558                "blocked": {  # this is an array of blocked currency
3559                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3560                },
3561                "blockedGuarantee": {  # this is locked money under collateral for futures
3562                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3563                },
3564            },
3565        }
3566
3567        # --- Prepare text table with limits in human-readable format:
3568        if show:
3569            info = [
3570                "# Withdrawal limits\n\n",
3571                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3572                "* **Account ID:** [{}]\n".format(self.accountId),
3573            ]
3574
3575            if view["limits"]["money"]:
3576                info.extend([
3577                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3578                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3579                ])
3580
3581            else:
3582                info.append("\nNo withdrawal limits\n")
3583
3584            for curr in view["limits"]["money"].keys():
3585                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3586                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3587                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3588
3589                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3590                    "[{}]".format(curr),
3591                    "{:.2f}".format(view["limits"]["money"][curr]),
3592                    "{:.2f}".format(availableMoney),
3593                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3594                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3595                )
3596
3597                if curr == "rub":
3598                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3599
3600                else:
3601                    info.append(infoStr)
3602
3603            infoText = "".join(info)
3604
3605            uLogger.info(infoText)
3606
3607            if self.withdrawalLimitsFile:
3608                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3609                    fH.write(infoText)
3610
3611                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3612
3613        return view
3614
3615    def RequestAccounts(self) -> dict:
3616        """
3617        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3618
3619        See also:
3620        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3621        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3622        - `OverviewUserInfo()` method
3623
3624        :return: dict with raw data from server that contains accounts info. Example of dict:
3625                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3626                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3627                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3628                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3629        """
3630        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3631
3632        self.body = str({})
3633        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3634        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3635
3636        if self.moreDebug:
3637            uLogger.debug("Records about available accounts successfully received")
3638
3639        return rawAccounts
3640
3641    def RequestUserInfo(self) -> dict:
3642        """
3643        Method for requesting common user's information.
3644
3645        See also:
3646        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3647        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3648        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3649        - `OverviewUserInfo()` method
3650
3651        :return: dict with raw data from server that contains user's information. Example of dict:
3652                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3653                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3654        """
3655        uLogger.debug("Requesting common user's information. Wait, please...")
3656
3657        self.body = str({})
3658        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3659        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3660
3661        if self.moreDebug:
3662            uLogger.debug("Records about current user successfully received")
3663
3664        return rawUserInfo
3665
3666    def RequestMarginStatus(self, accountId: str = None) -> dict:
3667        """
3668        Method for requesting margin calculation for defined account ID.
3669
3670        See also:
3671        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3672        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3673        - `OverviewUserInfo()` method
3674
3675        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3676        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3677                 Example of responses:
3678                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3679                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3680                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3681                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3682                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3683                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3684        """
3685        if accountId is None or not accountId:
3686            if self.accountId is None or not self.accountId:
3687                uLogger.error("Variable `accountId` must be defined for using this method!")
3688                raise Exception("Account ID required")
3689
3690            else:
3691                accountId = self.accountId  # use `self.accountId` (main ID) by default
3692
3693        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3694
3695        self.body = str({"accountId": accountId})
3696        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3697        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3698
3699        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3700            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3701            rawMargin = {}
3702
3703        else:
3704            if self.moreDebug:
3705                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3706
3707        return rawMargin
3708
3709    def RequestTariffLimits(self) -> dict:
3710        """
3711        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3712
3713        See also:
3714        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3715        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3716        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3717        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3718        - `OverviewUserInfo()` method
3719
3720        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3721                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3722                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3723        """
3724        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3725
3726        self.body = str({})
3727        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3728        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3729
3730        if self.moreDebug:
3731            uLogger.debug("Records with limits of current tariff successfully received")
3732
3733        return rawTariffLimits
3734
3735    def RequestBondCoupons(self, iJSON: dict) -> dict:
3736        """
3737        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3738        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3739        All dates are in UTC timezone.
3740
3741        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3742        Documentation:
3743        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3744        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3745
3746        See also: `ExtendBondsData()`.
3747
3748        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3749                      If raw iJSON is not data of bond then server returns an error [400] with message:
3750                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3751        :return: dictionary with bond payment calendar. Response example
3752                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3753                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3754                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3755                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3756        """
3757        if iJSON["figi"] is None or not iJSON["figi"]:
3758            uLogger.error("FIGI must be defined for using this method!")
3759            raise Exception("FIGI required")
3760
3761        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3762        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3763
3764        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3765            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3766            self.figi,
3767            startDate,
3768            endDate,
3769        ))
3770
3771        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3772        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3773        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3774
3775        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3776            uLogger.warning("Instrument type is not bond!")
3777
3778        else:
3779            if self.moreDebug:
3780                uLogger.debug("Records about bond payment calendar successfully received")
3781
3782        return calendar
3783
3784    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3785        """
3786        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3787        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3788        coupon yields, current yields and some statistics etc.
3789
3790        WARNING! This is too long operation if a lot of bonds requested from broker server.
3791
3792        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3793
3794        :param instruments: list of strings with tickers or FIGIs.
3795        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3796                     for further used by data scientists or stock analytics.
3797        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3798                 In XLSX-file and Pandas DataFrame fields mean:
3799                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3800                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3801        """
3802        if instruments is None or not instruments:
3803            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3804            raise Exception("Ticker or FIGI required")
3805
3806        if isinstance(instruments, str):
3807            instruments = [instruments]
3808
3809        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3810
3811        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3812
3813        iCount = len(uniqueInstruments)
3814        tooLong = iCount >= 20
3815        if tooLong:
3816            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3817
3818        bonds = None
3819        for i, self.figi in enumerate(uniqueInstruments):
3820            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3821
3822            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3823                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3824                rawBond = self.SearchByFIGI(requestPrice=True)
3825
3826                # Widen raw data with UTC current time (iData["actualDateTime"]):
3827                actualDate = datetime.now(tzutc())
3828                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3829
3830                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3831                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3832
3833                # Replace some values with human-readable:
3834                iData["nominalCurrency"] = iData["nominal"]["currency"]
3835                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3836                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3837                iData["aciCurrency"] = iData["aciValue"]["currency"]
3838                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3839                iData["issueSize"] = int(iData["issueSize"])
3840                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3841                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3842                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3843                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3844                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3845                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3846                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3847                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3848                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3849                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3850
3851                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3852                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3853                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3854                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3855                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3856                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3857                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3858                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3859                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3860                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3861                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3862
3863                # Widen raw data with calendar data from `rawCalendar` values:
3864                calendarData = []
3865                if "events" in iData["rawCalendar"].keys():
3866                    for item in iData["rawCalendar"]["events"]:
3867                        calendarData.append({
3868                            "couponDate": item["couponDate"],
3869                            "couponNumber": int(item["couponNumber"]),
3870                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3871                            "payCurrency": item["payOneBond"]["currency"],
3872                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3873                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3874                            "couponStartDate": item["couponStartDate"],
3875                            "couponEndDate": item["couponEndDate"],
3876                            "couponPeriod": item["couponPeriod"],
3877                        })
3878
3879                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3880                    if "maturityDate" not in iData.keys():
3881                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3882
3883                # Widen raw data with Coupon Rate.
3884                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3885                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3886                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3887                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3888
3889                # Widen raw data with Yield to Maturity (YTM) on current date.
3890                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3891                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3892                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3893                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3894                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3895                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3896
3897                iData["calendar"] = calendarData  # adds calendar at the end
3898
3899                # Remove not used data:
3900                iData.pop("uid")
3901                iData.pop("positionUid")
3902                iData.pop("currentPrice")
3903                iData.pop("rawCalendar")
3904
3905                colNames = list(iData.keys())
3906                if bonds is None:
3907                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3908
3909                else:
3910                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3911
3912            else:
3913                uLogger.warning("Instrument is not a bond!")
3914
3915            processed = round(100 * (i + 1) / iCount, 1)
3916            if tooLong and processed % 5 == 0:
3917                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3918
3919            else:
3920                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3921
3922        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3923
3924        # Saving bonds from Pandas DataFrame to XLSX sheet:
3925        if xlsx and self.bondsXLSXFile:
3926            with pd.ExcelWriter(
3927                    path=self.bondsXLSXFile,
3928                    date_format=TKS_DATE_FORMAT,
3929                    datetime_format=TKS_DATE_TIME_FORMAT,
3930                    mode="w",
3931            ) as writer:
3932                bonds.to_excel(
3933                    writer,
3934                    sheet_name="Extended bonds data",
3935                    index=True,
3936                    encoding="UTF-8",
3937                    freeze_panes=(1, 1),
3938                )  # saving as XLSX-file with freeze first row and column as headers
3939
3940            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3941
3942        return bonds
3943
3944    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3945        """
3946        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3947
3948        WARNING! This is too long operation if a lot of bonds requested from broker server.
3949
3950        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3951
3952        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3953                        extended information about bonds: main info, current prices, bond payment calendar,
3954                        coupon yields, current yields and some statistics etc.
3955                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3956        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3957                     for further used by data scientists or stock analytics.
3958        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3959        """
3960        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3961            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3962
3963        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3964
3965        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3966        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3967        calendar = None
3968        for bond in extBonds.iterrows():
3969            for item in bond[1]["calendar"]:
3970                cData = {
3971                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3972                    "couponDate": item["couponDate"],
3973                    "figi": bond[1]["figi"],
3974                    "ticker": bond[1]["ticker"],
3975                    "name": bond[1]["name"],
3976                    "couponNumber": item["couponNumber"],
3977                    "payOneBond": item["payOneBond"],
3978                    "payCurrency": item["payCurrency"],
3979                    "couponType": item["couponType"],
3980                    "couponPeriod": item["couponPeriod"],
3981                    "fixDate": item["fixDate"],
3982                    "couponStartDate": item["couponStartDate"],
3983                    "couponEndDate": item["couponEndDate"],
3984                }
3985
3986                if calendar is None:
3987                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3988
3989                else:
3990                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
3991
3992        if calendar is not None:
3993            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
3994
3995            # Saving calendar from Pandas DataFrame to XLSX sheet:
3996            if xlsx:
3997                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
3998
3999                with pd.ExcelWriter(
4000                        path=xlsxCalendarFile,
4001                        date_format=TKS_DATE_FORMAT,
4002                        datetime_format=TKS_DATE_TIME_FORMAT,
4003                        mode="w",
4004                ) as writer:
4005                    humanReadable = calendar.copy(deep=True)
4006                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4007                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4008                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4009                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4010                    humanReadable.columns = colNames  # human-readable column names
4011
4012                    humanReadable.to_excel(
4013                        writer,
4014                        sheet_name="Bond payments calendar",
4015                        index=False,
4016                        encoding="UTF-8",
4017                        freeze_panes=(1, 2),
4018                    )  # saving as XLSX-file with freeze first row and column as headers
4019
4020                    del humanReadable  # release df in memory
4021
4022                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4023
4024        return calendar
4025
4026    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4027        """
4028        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4029        Also, creates Markdown file with calendar data, `calendar.md` by default.
4030
4031        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4032
4033        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4034                        extended information about bonds: main info, current prices, bond payment calendar,
4035                        coupon yields, current yields and some statistics etc.
4036                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4037        :param show: if `True` then also printing bonds payment calendar to the console,
4038                     otherwise save to file `calendarFile` only. `False` by default.
4039        :return: multilines text in Markdown format with bonds payment calendar as a table.
4040        """
4041        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4042            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4043
4044        infoText = "# Bond payments calendar\n\n"
4045
4046        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4047
4048        if not (calendar is None or calendar.empty):
4049            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4050
4051            info = [
4052                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4053                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4054            ]
4055
4056            newMonth = False
4057            notOneBond = calendar["figi"].nunique() > 1
4058            for i, bond in enumerate(calendar.iterrows()):
4059                if newMonth and notOneBond:
4060                    info.append(splitLine)
4061
4062                info.append(
4063                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4064                        "  √" if bond[1]["paid"] else "  —",
4065                        bond[1]["couponDate"].split("T")[0],
4066                        bond[1]["figi"],
4067                        bond[1]["ticker"],
4068                        bond[1]["couponNumber"],
4069                        "{} {}".format(
4070                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4071                            bond[1]["payCurrency"],
4072                        ),
4073                        bond[1]["couponType"],
4074                        bond[1]["couponPeriod"],
4075                        bond[1]["fixDate"].split("T")[0],
4076                    )
4077                )
4078
4079                if i < len(calendar.values) - 1:
4080                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4081                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4082                    newMonth = False if curDate.month == nextDate.month else True
4083
4084                else:
4085                    newMonth = False
4086
4087            infoText += "".join(info)
4088
4089            if show:
4090                uLogger.info("{}".format(infoText))
4091
4092            if self.calendarFile is not None:
4093                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4094                    fH.write(infoText)
4095
4096                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4097
4098        else:
4099            infoText += "No data\n"
4100
4101        return infoText
4102
4103    def OverviewAccounts(self, show: bool = False) -> dict:
4104        """
4105        Method for parsing and show simple table with all available user accounts.
4106
4107        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4108
4109        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4110        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4111                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4112                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4113                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4114                                                        "closed": "—", "access": "Full access" }, ...}}`
4115        """
4116        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4117
4118        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4119        accounts = {
4120            item["id"]: {
4121                "type": TKS_ACCOUNT_TYPES[item["type"]],
4122                "name": item["name"],
4123                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4124                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4125                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4126                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4127            } for item in rawAccounts["accounts"]
4128        }
4129
4130        # Raw and parsed data with some fields replaced in "stat" section:
4131        view = {
4132            "rawAccounts": rawAccounts,
4133            "stat": accounts,
4134        }
4135
4136        # --- Prepare simple text table with only accounts data in human-readable format:
4137        if show:
4138            info = [
4139                "# User accounts\n\n",
4140                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4141                "| Account ID   | Type                      | Status                    | Name                           |\n",
4142                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4143            ]
4144
4145            for account in view["stat"].keys():
4146                info.extend([
4147                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4148                        account,
4149                        view["stat"][account]["type"],
4150                        view["stat"][account]["status"],
4151                        view["stat"][account]["name"],
4152                    )
4153                ])
4154
4155            infoText = "".join(info)
4156
4157            uLogger.info(infoText)
4158
4159            if self.userAccountsFile:
4160                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4161                    fH.write(infoText)
4162
4163                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4164
4165        return view
4166
4167    def OverviewUserInfo(self, show: bool = False) -> dict:
4168        """
4169        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4170
4171        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4172
4173        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4174        :return: dict with raw parsed data from server and some calculated statistics about it.
4175        """
4176        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4177        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4178        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4179        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4180        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4181        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4182
4183        # This is dict with parsed common user data:
4184        userInfo = {
4185            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4186            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4187            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4188            "tariff": rawUserInfo["tariff"],
4189        }
4190
4191        # This is an array of dict with parsed margin statuses for every account IDs:
4192        margins = {}
4193        for accountId in accounts.keys():
4194            if rawMargins[accountId]:
4195                margins[accountId] = {
4196                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4197                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4198                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4199                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4200                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4201                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4202                }
4203
4204            else:
4205                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4206
4207        unary = {}  # unary-connection limits
4208        for item in rawTariffLimits["unaryLimits"]:
4209            if item["limitPerMinute"] in unary.keys():
4210                unary[item["limitPerMinute"]].extend(item["methods"])
4211
4212            else:
4213                unary[item["limitPerMinute"]] = item["methods"]
4214
4215        stream = {}  # stream-connection limits
4216        for item in rawTariffLimits["streamLimits"]:
4217            if item["limit"] in stream.keys():
4218                stream[item["limit"]].extend(item["streams"])
4219
4220            else:
4221                stream[item["limit"]] = item["streams"]
4222
4223        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4224        limits = {
4225            "unary": unary,
4226            "stream": stream,
4227        }
4228
4229        # Raw and parsed data as an output result:
4230        view = {
4231            "rawUserInfo": rawUserInfo,
4232            "rawAccounts": rawAccounts,
4233            "rawMargins": rawMargins,
4234            "rawTariffLimits": rawTariffLimits,
4235            "stat": {
4236                "userInfo": userInfo,
4237                "accounts": accounts,
4238                "margins": margins,
4239                "limits": limits,
4240            },
4241        }
4242
4243        # --- Prepare text table with user information in human-readable format:
4244        if show:
4245            info = [
4246                "# Full user information\n\n",
4247                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4248                "## Common information\n\n",
4249                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4250                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4251                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4252                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4253                "\n## User accounts\n\n",
4254            ]
4255
4256            for account in view["stat"]["accounts"].keys():
4257                info.extend([
4258                    "### ID: [{}]\n\n".format(account),
4259                    "| Parameters           | Values                                                       |\n",
4260                    "|----------------------|--------------------------------------------------------------|\n",
4261                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4262                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4263                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4264                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4265                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4266                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4267                ])
4268
4269                if margins[account]:
4270                    info.extend([
4271                        "| Margin status:       | Enabled                                                      |\n",
4272                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4273                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4274                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4275                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4276                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4277                    ])
4278
4279                else:
4280                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4281
4282            info.extend([
4283                "\n## Current user tariff limits\n",
4284                "\nSee also:\n",
4285                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4286                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4287                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4288                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4289                "\n### Unary limits\n",
4290            ])
4291
4292            if unary:
4293                for key, values in sorted(unary.items()):
4294                    info.append("\n* Max requests per minute: {}\n".format(key))
4295
4296                    for value in values:
4297                        info.append("  - {}\n".format(value))
4298
4299            else:
4300                info.append("\nNot available\n")
4301
4302            info.append("\n### Stream limits\n")
4303
4304            if stream:
4305                for key, values in sorted(stream.items()):
4306                    info.append("\n* Max stream connections: {}\n".format(key))
4307
4308                    for value in values:
4309                        info.append("  - {}\n".format(value))
4310
4311            else:
4312                info.append("\nNot available\n")
4313
4314            infoText = "".join(info)
4315
4316            uLogger.info(infoText)
4317
4318            if self.userInfoFile:
4319                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4320                    fH.write(infoText)
4321
4322                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4323
4324        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 86        """
 87        Main class init.
 88
 89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 92        :param useCache: use default cache file with raw data to use instead of `iList`.
 93                         True by default. Cache is auto-update if new day has come.
 94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 95        :param defaultCache: path to default cache file. `dump.json` by default.
 96        """
 97        if token is None or not token:
 98            try:
 99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
101
102            except KeyError:
103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
104                raise Exception("Token required")
105
106        else:
107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
109
110        if accountId is None or not accountId:
111            try:
112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
114
115            except KeyError:
116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
117
118        else:
119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
121
122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
124
125        Latest version: https://pypi.org/project/tksbrokerapi/
126        """
127
128        self.aliases = TKS_TICKER_ALIASES
129        """Some aliases instead official tickers.
130
131        See also: `TKSEnums.TKS_TICKER_ALIASES`
132        """
133
134        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
135
136        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
137
138        self.ticker = ""
139        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
140
141        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
142        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
143
144        See also: `SearchByTicker()`, `SearchInstruments()`.
145        """
146
147        self.figi = ""
148        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
149
150        See also: `SearchByFIGI()`, `SearchInstruments()`.
151        """
152
153        self.depth = 1
154        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
155
156        See also: `GetCurrentPrices()`.
157        """
158
159        self.server = r"https://invest-public-api.tinkoff.ru/rest"
160        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
161
162        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
163        """
164
165        uLogger.debug("Broker API server: {}".format(self.server))
166
167        self.timeout = 15
168        """Server operations timeout in seconds. Default: `15`.
169
170        See also: `SendAPIRequest()`.
171        """
172
173        self.headers = {
174            "Content-Type": "application/json",
175            "accept": "application/json",
176            "Authorization": "Bearer {}".format(self.token),
177            "x-app-name": "Tim55667757.TKSBrokerAPI",
178        }
179        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
180
181        See also: `SendAPIRequest()`.
182        """
183
184        self.body = None
185        """Request body which send to broker server. Default: `None`.
186
187        See also: `SendAPIRequest()`.
188        """
189
190        self.moreDebug = False
191        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
192
193        self.historyFile = None
194        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
195
196        See also: `History()`.
197        """
198
199        self.htmlHistoryFile = "index.html"
200        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
201
202        See also: `ShowHistoryChart()`.
203        """
204
205        self.instrumentsFile = "instruments.md"
206        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
207
208        See also: `ShowInstrumentsInfo()`.
209        """
210
211        self.searchResultsFile = "search-results.md"
212        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
213
214        See also: `SearchInstruments()`.
215        """
216
217        self.pricesFile = "prices.md"
218        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
219
220        See also: `GetListOfPrices()`.
221        """
222
223        self.infoFile = "info.md"
224        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
225
226        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
227        """
228
229        self.bondsXLSXFile = "ext-bonds.xlsx"
230        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
231        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
232
233        See also: `ExtendBondsData()`.
234        """
235
236        self.calendarFile = "calendar.md"
237        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
238        
239        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
240
241        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
242        """
243
244        self.overviewFile = "overview.md"
245        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
246
247        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
248        """
249
250        self.overviewDigestFile = "overview-digest.md"
251        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
252
253        See also: `Overview()` with parameter `details="digest"`.
254        """
255
256        self.overviewPositionsFile = "overview-positions.md"
257        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
258
259        See also: `Overview()` with parameter `details="positions"`.
260        """
261
262        self.overviewOrdersFile = "overview-orders.md"
263        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
264
265        See also: `Overview()` with parameter `details="orders"`.
266        """
267
268        self.overviewAnalyticsFile = "overview-analytics.md"
269        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
270
271        See also: `Overview()` with parameter `details="analytics"`.
272        """
273
274        self.overviewBondsCalendarFile = "overview-calendar.md"
275        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
276
277        See also: `Overview()` with parameter `details="calendar"`.
278        """
279
280        self.reportFile = "deals.md"
281        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
282
283        See also: `Deals()`.
284        """
285
286        self.withdrawalLimitsFile = "limits.md"
287        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
288
289        See also: `OverviewLimits()` and `RequestLimits()`.
290        """
291
292        self.userInfoFile = "user-info.md"
293        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
294
295        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
296        """
297
298        self.userAccountsFile = "accounts.md"
299        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
300
301        See also: `OverviewAccounts()`, `RequestAccounts()`.
302        """
303
304        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
305        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
306
307        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
308
309        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
310        """
311
312        self.iList = None  # init iList for raw instruments data
313        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
314        
315        See also: `Listing()`, `DumpInstruments()`.
316        """
317
318        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
319        if useCache:
320            if os.path.exists(self.iListDumpFile):
321                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
322                curTime = datetime.now(tzutc())
323
324                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
325                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
326
327                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
328
329                else:
330                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
331
332                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
333                        os.path.abspath(self.iListDumpFile),
334                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
335                    ))
336
337            else:
338                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
339                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
340
341        else:
342            self.iList = self.Listing()  # request new raw instruments data from broker server
343            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
344
345        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
346        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
347
348        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
349        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

ticker

String with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi

String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
365    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
366        """
367        Send GET or POST request to broker server and receive JSON object.
368
369        self.header: must be defining with dictionary of headers.
370        self.body: if define then used as request body. None by default.
371        self.timeout: global request timeout, 15 seconds by default.
372        :param url: url with REST request.
373        :param reqType: send "GET" or "POST" request. "GET" by default.
374        :param retry: how many times retry after first request if an 5xx server errors occurred.
375        :param pause: sleep time in seconds between retries.
376        :return: response JSON (dictionary) from broker.
377        """
378        if reqType not in ("GET", "POST"):
379            uLogger.error("You can define request type: 'GET' or 'POST'!")
380            raise Exception("Incorrect value")
381
382        if self.moreDebug:
383            uLogger.debug("Request parameters:")
384            uLogger.debug("    - REST API URL: {}".format(url))
385            uLogger.debug("    - request type: {}".format(reqType))
386            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
387            uLogger.debug("    - body:\n{}".format(self.body))
388
389        # fast hack to avoid all operations with some tickers/FIGI
390        responseJSON = {}
391        oK = True
392        for item in self.exclude:
393            if item in url:
394                if self.moreDebug:
395                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
396
397                oK = False
398                break
399
400        if oK:
401            counter = 0
402            response = None
403            errMsg = ""
404
405            while not response and counter <= retry:
406                if reqType == "GET":
407                    response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
408
409                if reqType == "POST":
410                    response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
411
412                if self.moreDebug:
413                    uLogger.debug("Response:")
414                    uLogger.debug("    - status code: {}".format(response.status_code))
415                    uLogger.debug("    - reason: {}".format(response.reason))
416                    uLogger.debug("    - body length: {}".format(len(response.text)))
417                    uLogger.debug("    - headers:\n{}".format(response.headers))
418
419                # Server returns some headers:
420                # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
421                # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
422                # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
423                # See: https://tinkoff.github.io/investAPI/grpc/#kreya
424                if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
425                    rateLimitWait = int(response.headers["x-ratelimit-reset"])
426                    uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
427                    sleep(rateLimitWait)
428
429                # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
430                if 400 <= response.status_code < 500:
431                    msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
432                    uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
433                    counter = retry + 1
434
435                if 500 <= response.status_code < 600:
436                    errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
437                    uLogger.debug("    - not oK, {}".format(errMsg))
438                    counter += 1
439
440                    if counter <= retry:
441                        uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
442                        sleep(pause)
443
444            responseJSON = self._ParseJSON(rawData=response.text)
445
446            if errMsg:
447                uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
448                uLogger.error("    - not oK, {}".format(errMsg))
449
450        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
483    def Listing(self) -> dict:
484        """
485        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
486
487        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
488        """
489        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
490        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
491
492        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
493        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
494        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
495
496        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
497        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
498        poolUpdater.close()
499
500        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
501        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
502        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
503
504        # calculate minimum price increment (step) for all instruments and set up instrument's type:
505        for iType in iList.keys():
506            for ticker in iList[iType]:
507                iList[iType][ticker]["type"] = iType
508
509                if "minPriceIncrement" in iList[iType][ticker].keys():
510                    iList[iType][ticker]["step"] = NanoToFloat(
511                        iList[iType][ticker]["minPriceIncrement"]["units"],
512                        iList[iType][ticker]["minPriceIncrement"]["nano"],
513                    )
514
515                else:
516                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
517
518        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
520    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
521        """
522        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
523
524        See also: `DumpInstruments()`, `Listing()`.
525
526        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
527                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
528        """
529        if self.iListDumpFile is None or not self.iListDumpFile:
530            uLogger.error("Output name of dump file must be defined!")
531            raise Exception("Filename required")
532
533        if not self.iList or forceUpdate:
534            self.iList = self.Listing()
535
536        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
537
538        # Save as XLSX with separated sheets for every type of instruments:
539        with pd.ExcelWriter(
540                path=xlsxDumpFile,
541                date_format=TKS_DATE_FORMAT,
542                datetime_format=TKS_DATE_TIME_FORMAT,
543                mode="w",
544        ) as writer:
545            for iType in TKS_INSTRUMENTS:
546                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
547                df = df[sorted(df)]  # sorted by column names
548                df = df.applymap(
549                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
550                    na_action="ignore",
551                )  # converting numbers from nano-type to float in every cell
552                df.to_excel(
553                    writer,
554                    sheet_name=iType,
555                    encoding="UTF-8",
556                    freeze_panes=(1, 1),
557                )  # saving as XLSX-file with freeze first row and column as headers
558
559        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
561    def DumpInstruments(self, forceUpdate: bool = True) -> str:
562        """
563        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
564        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
565
566        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
567
568        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
569                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
570        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
571        """
572        if self.iListDumpFile is None or not self.iListDumpFile:
573            uLogger.error("Output name of dump file must be defined!")
574            raise Exception("Filename required")
575
576        if not self.iList or forceUpdate:
577            self.iList = self.Listing()
578
579        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
580        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
581            fH.write(jsonDump)
582
583        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
584
585        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
587    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
588        """
589        Show information about one instrument defined by json data and prints it in Markdown format.
590
591        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
592
593        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]`
594        :param show: if `True` then also printing information about instrument and its current price.
595        :return: multilines text in Markdown format with information about one instrument.
596        """
597        splitLine = "|                                                             |                                                        |\n"
598        infoText = ""
599
600        if iJSON is not None and iJSON and isinstance(iJSON, dict):
601            info = [
602                "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]),
603                "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
604                "| Parameters                                                  | Values                                                 |\n",
605                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
606                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
607                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
608            ]
609
610            if "sector" in iJSON.keys() and iJSON["sector"]:
611                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
612
613            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
614                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
615
616            info.extend([
617                splitLine,
618                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
619                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
620            ])
621
622            if "isin" in iJSON.keys() and iJSON["isin"]:
623                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
624
625            if "classCode" in iJSON.keys():
626                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
627
628            info.extend([
629                splitLine,
630                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
631                splitLine,
632                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
633                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
634                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
635            ])
636
637            if iJSON["figi"]:
638                self.figi = iJSON["figi"]
639                iJSON = iJSON | self.RequestTradingStatus()
640
641                info.extend([
642                    splitLine,
643                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
644                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
645                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
646                ])
647
648            info.append(splitLine)
649
650            if "type" in iJSON.keys() and iJSON["type"]:
651                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
652
653                if "shareType" in iJSON.keys() and iJSON["shareType"]:
654                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
655
656            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
657                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
658
659            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
660                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
661
662            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
663                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
664
665            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
666                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
667
668            if "focusType" in iJSON.keys() and iJSON["focusType"]:
669                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
670
671            if "assetType" in iJSON.keys() and iJSON["assetType"]:
672                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
673
674            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
675                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
676
677            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
678                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
679
680            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
681                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
682
683            if "currency" in iJSON.keys():
684                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
685
686            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
687                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
688
689            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
690                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
691
692            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
693                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
694
695            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
696                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
697
698            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
699                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
700
701            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
702                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
703
704            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
705                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
706
707            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
708                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
709
710            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
711                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
712
713            iExt = None
714            if iJSON["type"] == "Bonds":
715                info.extend([
716                    splitLine,
717                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
718                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
719                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
720                        iJSON["nominal"]["currency"],
721                    )),
722                ])
723
724                if "floatingCouponFlag" in iJSON.keys():
725                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
726
727                if "amortizationFlag" in iJSON.keys():
728                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
729
730                info.append(splitLine)
731
732                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
733                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
734
735                if iJSON["figi"]:
736                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
737
738                    info.extend([
739                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
740                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
741                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
742                    ])
743
744                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
745                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
746                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
747                        iJSON["aciValue"]["currency"]
748                    )))
749
750            if "currentPrice" in iJSON.keys():
751                info.append(splitLine)
752
753                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
754                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
755
756                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
757                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
758                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
759                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
760                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
761
762                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
763                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
764
765                info.extend([
766                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
767                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
768                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
769                    )),
770                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
771                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
772                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
773                    )),
774                    "| Changes between last deal price and last close              | {:<54} |\n".format(
775                        "{:.2f}%{}".format(
776                            iJSON["currentPrice"]["changes"],
777                            " ({}{:.2f} {})".format(
778                                "+" if bondChangesDelta > 0 else "",
779                                bondChangesDelta,
780                                aciCurrency
781                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
782                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
783                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
784                                currency
785                            ),
786                        )
787                    ),
788                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
789                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
790                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
791                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
792                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
793                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
794                    )),
795                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
796                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
797                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
798                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
799                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
800                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
801                    )),
802                ])
803
804            if "lot" in iJSON.keys():
805                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
806
807            if "step" in iJSON.keys() and iJSON["step"] != 0:
808                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
809
810            # Add bond payment calendar:
811            if iJSON["type"] == "Bonds":
812                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
813                info.extend(["\n", strCalendar])
814
815            infoText += "".join(info)
816
817            if show:
818                uLogger.info("{}".format(infoText))
819
820            else:
821                uLogger.debug("{}".format(infoText))
822
823            if self.infoFile is not None:
824                with open(self.infoFile, "w", encoding="UTF-8") as fH:
825                    fH.write(infoText)
826
827                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
828
829        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self.ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
831    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
832        """
833        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
834
835        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
836        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
837        :return: JSON formatted data with information about instrument.
838        """
839        tickerJSON = {}
840        if self.moreDebug:
841            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker))
842
843        if not self.ticker:
844            uLogger.warning("self.ticker variable is not be empty!")
845
846        else:
847            if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
848                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker))
849                raise Exception("Instrument not allowed")
850
851            if not self.iList:
852                self.iList = self.Listing()
853
854            if self.ticker in self.iList["Shares"].keys():
855                tickerJSON = self.iList["Shares"][self.ticker]
856                if self.moreDebug:
857                    uLogger.debug("Ticker [{}] found in shares list".format(self.ticker))
858
859            elif self.ticker in self.iList["Currencies"].keys():
860                tickerJSON = self.iList["Currencies"][self.ticker]
861                if self.moreDebug:
862                    uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker))
863
864            elif self.ticker in self.iList["Bonds"].keys():
865                tickerJSON = self.iList["Bonds"][self.ticker]
866                if self.moreDebug:
867                    uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker))
868
869            elif self.ticker in self.iList["Etfs"].keys():
870                tickerJSON = self.iList["Etfs"][self.ticker]
871                if self.moreDebug:
872                    uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker))
873
874            elif self.ticker in self.iList["Futures"].keys():
875                tickerJSON = self.iList["Futures"][self.ticker]
876                if self.moreDebug:
877                    uLogger.debug("Ticker [{}] found in futures list".format(self.ticker))
878
879        if tickerJSON:
880            self.figi = tickerJSON["figi"]
881
882            if requestPrice:
883                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
884
885                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
886                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
887
888                else:
889                    tickerJSON["currentPrice"]["changes"] = 0
890
891            if show:
892                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
893
894        else:
895            if show:
896                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker))
897
898        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
900    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
901        """
902        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
903
904        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
905        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
906        :return: JSON formatted data with information about instrument.
907        """
908        figiJSON = {}
909        if self.moreDebug:
910            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi))
911
912        if not self.figi:
913            uLogger.warning("self.figi variable is not be empty!")
914
915        else:
916            if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
917                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi))
918                raise Exception("Instrument not allowed")
919
920            if not self.iList:
921                self.iList = self.Listing()
922
923            for item in self.iList["Shares"].keys():
924                if self.figi == self.iList["Shares"][item]["figi"]:
925                    figiJSON = self.iList["Shares"][item]
926
927                    if self.moreDebug:
928                        uLogger.debug("FIGI [{}] found in shares list".format(self.figi))
929
930                    break
931
932            if not figiJSON:
933                for item in self.iList["Currencies"].keys():
934                    if self.figi == self.iList["Currencies"][item]["figi"]:
935                        figiJSON = self.iList["Currencies"][item]
936
937                        if self.moreDebug:
938                            uLogger.debug("FIGI [{}] found in currencies list".format(self.figi))
939
940                        break
941
942            if not figiJSON:
943                for item in self.iList["Bonds"].keys():
944                    if self.figi == self.iList["Bonds"][item]["figi"]:
945                        figiJSON = self.iList["Bonds"][item]
946
947                        if self.moreDebug:
948                            uLogger.debug("FIGI [{}] found in bonds list".format(self.figi))
949
950                        break
951
952            if not figiJSON:
953                for item in self.iList["Etfs"].keys():
954                    if self.figi == self.iList["Etfs"][item]["figi"]:
955                        figiJSON = self.iList["Etfs"][item]
956
957                        if self.moreDebug:
958                            uLogger.debug("FIGI [{}] found in etfs list".format(self.figi))
959
960                        break
961
962            if not figiJSON:
963                for item in self.iList["Futures"].keys():
964                    if self.figi == self.iList["Futures"][item]["figi"]:
965                        figiJSON = self.iList["Futures"][item]
966
967                        if self.moreDebug:
968                            uLogger.debug("FIGI [{}] found in futures list".format(self.figi))
969
970                        break
971
972        if figiJSON:
973            self.figi = figiJSON["figi"]
974            self.ticker = figiJSON["ticker"]
975
976            if requestPrice:
977                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
978
979                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
980                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
981
982                else:
983                    figiJSON["currentPrice"]["changes"] = 0
984
985            if show:
986                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
987
988        else:
989            if show:
990                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi))
991
992        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
 994    def GetCurrentPrices(self, show: bool = True) -> dict:
 995        """
 996        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
 997        `{"buy": [{"price": 1243.8, "quantity": 193},
 998                  {"price": 1244.0, "quantity": 168},
 999                  {"price": 1244.8, "quantity": 5},
1000                  {"price": 1245.0, "quantity": 61},
1001                  {"price": 1245.4, "quantity": 60}],
1002          "sell": [{"price": 1243.6, "quantity": 8},
1003                   {"price": 1242.6, "quantity": 10},
1004                   {"price": 1242.4, "quantity": 18},
1005                   {"price": 1242.2, "quantity": 50},
1006                   {"price": 1242.0, "quantity": 113}],
1007          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1008        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1009        - sell: list of dicts with Buyers prices,
1010            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1011            - quantity: volume value by current price in lots,
1012        - limitUp: current trade session limit price, maximum,
1013        - limitDown: current trade session limit price, minimum,
1014        - lastPrice: last deal price of the instrument,
1015        - closePrice: previous trade session close price of the instrument.
1016
1017        See also: `SearchByTicker()` and `SearchByFIGI()`.
1018        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1019        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1020
1021        :param show: if `True` then print DOM to log and console.
1022        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1023                 If an error occurred then returns an empty record:
1024                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1025        """
1026        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1027
1028        if self.depth < 1:
1029            uLogger.error("Depth of Market (DOM) must be >=1!")
1030            raise Exception("Incorrect value")
1031
1032        if not (self.ticker or self.figi):
1033            uLogger.error("self.ticker or self.figi variables must be defined!")
1034            raise Exception("Ticker or FIGI required")
1035
1036        if self.ticker and not self.figi:
1037            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1038            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1039
1040        if not self.ticker and self.figi:
1041            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1042            self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1043
1044        if not self.figi:
1045            uLogger.error("FIGI is not defined!")
1046            raise Exception("Ticker or FIGI required")
1047
1048        else:
1049            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi))
1050
1051            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1052            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1053            self.body = str({"figi": self.figi, "depth": self.depth})
1054            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1055
1056            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1057                # list of dicts with sellers orders:
1058                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1059
1060                # list of dicts with buyers orders:
1061                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1062
1063                # max price of instrument at this time:
1064                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1065
1066                # min price of instrument at this time:
1067                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1068
1069                # last price of deal with instrument:
1070                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1071
1072                # last close price of instrument:
1073                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1074
1075            else:
1076                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1077                uLogger.debug("Server response: {}".format(pricesResponse))
1078
1079            if show:
1080                if prices["buy"] or prices["sell"]:
1081                    info = [
1082                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1083                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1084                            self.ticker,
1085                            self.figi,
1086                            self.depth,
1087                        ),
1088                        "-" * 60, "\n",
1089                        "             Orders of Buyers | Orders of Sellers\n",
1090                        "-" * 60, "\n",
1091                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1092                        "-" * 60, "\n",
1093                    ]
1094
1095                    if not prices["buy"]:
1096                        info.append("                              | No orders!\n")
1097                        sumBuy = 0
1098
1099                    else:
1100                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1101                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1102                        for item in maxMinSorted:
1103                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1104
1105                    if not prices["sell"]:
1106                        info.append("No orders!                    |\n")
1107                        sumSell = 0
1108
1109                    else:
1110                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1111                        for item in prices["sell"]:
1112                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1113
1114                    info.extend([
1115                        "-" * 60, "\n",
1116                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1117                        "-" * 60, "\n",
1118                    ])
1119
1120                    infoText = "".join(info)
1121
1122                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1123
1124                else:
1125                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi))
1126
1127        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1129    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1130        """
1131        This method get and show information about all available broker instruments for current user account.
1132        If `instrumentsFile` string is not empty then also save information to this file.
1133
1134        :param show: if `True` then print results to console, if `False` — print only to file.
1135        :return: multi-lines string with all available broker instruments
1136        """
1137        if not self.iList:
1138            self.iList = self.Listing()
1139
1140        info = [
1141            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1142            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1143        ]
1144
1145        # add instruments count by type:
1146        for iType in self.iList.keys():
1147            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1148
1149        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1150        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1151
1152        # generating info tables with all instruments by type:
1153        for iType in self.iList.keys():
1154            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1155
1156            for instrument in self.iList[iType].keys():
1157                iName = self.iList[iType][instrument]["name"]  # instrument's name
1158                if len(iName) > 57:
1159                    iName = "{}...".format(iName[:54])  # right trim for a long string
1160
1161                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1162                    self.iList[iType][instrument]["ticker"],
1163                    iName,
1164                    self.iList[iType][instrument]["figi"],
1165                    self.iList[iType][instrument]["currency"],
1166                    self.iList[iType][instrument]["lot"],
1167                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1168                ))
1169
1170        infoText = "".join(info)
1171
1172        if show:
1173            uLogger.info(infoText)
1174
1175        if self.instrumentsFile:
1176            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1177                fH.write(infoText)
1178
1179            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1180
1181        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1183    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1184        """
1185        This method search and show information about instruments by part of its ticker, FIGI or name.
1186        If `searchResultsFile` string is not empty then also save information to this file.
1187
1188        :param pattern: string with part of ticker, FIGI or instrument's name.
1189        :param show: if `True` then print results to console, if `False` — return list of result only.
1190        :return: list of dictionaries with all found instruments.
1191        """
1192        if not self.iList:
1193            self.iList = self.Listing()
1194
1195        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contains only filtered instruments
1196        compiledPattern = re.compile(pattern, re.IGNORECASE)
1197
1198        for iType in self.iList:
1199            for instrument in self.iList[iType].values():
1200                searchResult = compiledPattern.search(" ".join(
1201                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1202                ))
1203
1204                if searchResult:
1205                    searchResults[iType][instrument["ticker"]] = instrument
1206
1207        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1208        info = [
1209            "# Search results\n\n",
1210            "* **Search pattern:** [{}]\n".format(pattern),
1211            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1212            "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n"
1213        ]
1214        infoShort = info[:]
1215
1216        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1217        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1218        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1219
1220        if resultsLen == 0:
1221            info.append("\nNo results\n")
1222            infoShort.append("\nNo results\n")
1223            uLogger.warning("No results. Try changing your search pattern.")
1224
1225        else:
1226            for iType in searchResults:
1227                iTypeValuesCount = len(searchResults[iType].values())
1228                if iTypeValuesCount > 0:
1229                    info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1230                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1231
1232                    for instrument in searchResults[iType].values():
1233                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1234                            instrument["type"],
1235                            instrument["ticker"],
1236                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1237                            instrument["figi"],
1238                        ))
1239
1240                    if iTypeValuesCount <= 5:
1241                        infoShort.extend(info[-iTypeValuesCount:])
1242
1243                    else:
1244                        infoShort.extend(info[-5:])
1245                        infoShort.append(skippedLine)
1246
1247        infoText = "".join(info)
1248        infoTextShort = "".join(infoShort)
1249
1250        if show:
1251            uLogger.info(infoTextShort)
1252            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1253
1254        if self.searchResultsFile:
1255            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1256                fH.write(infoText)
1257
1258            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1259
1260        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1262    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1263        """
1264        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1265
1266        :param instruments: list of strings with tickers or FIGIs.
1267        :return: list with unique instrument FIGIs only.
1268        """
1269        requestedInstruments = []
1270        for iName in instruments:
1271            if iName not in self.aliases.keys():
1272                if iName not in requestedInstruments:
1273                    requestedInstruments.append(iName)
1274
1275            else:
1276                if iName not in requestedInstruments:
1277                    if self.aliases[iName] not in requestedInstruments:
1278                        requestedInstruments.append(self.aliases[iName])
1279
1280        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1281
1282        onlyUniqueFIGIs = []
1283        for iName in requestedInstruments:
1284            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1285                continue
1286
1287            self.ticker = iName
1288            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1289
1290            if not iData:
1291                self.ticker = ""
1292                self.figi = iName
1293
1294                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1295
1296                if not iData:
1297                    self.figi = ""
1298                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1299
1300            if iData and iData["figi"] not in onlyUniqueFIGIs:
1301                onlyUniqueFIGIs.append(iData["figi"])
1302
1303        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1304
1305        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1307    def GetListOfPrices(self, instruments: list, show: bool = False) -> list:
1308        """
1309        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1310
1311        See limits: https://tinkoff.github.io/investAPI/limits/
1312
1313        If `pricesFile` string is not empty then also save information to this file.
1314
1315        :param instruments: list of strings with tickers or FIGIs.
1316        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1317        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1318                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1319        """
1320        if instruments is None or not instruments:
1321            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1322            raise Exception("Ticker or FIGI required")
1323
1324        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1325
1326        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1327
1328        iList = []  # trying to get info and current prices about all unique instruments:
1329        for self.figi in onlyUniqueFIGIs:
1330            iData = self.SearchByFIGI(requestPrice=True)
1331            iList.append(iData)
1332
1333        self.ShowListOfPrices(iList, show)
1334
1335        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1337    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1338        """
1339        Show table contains current prices of given instruments.
1340
1341        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1342                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1343        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1344        :return: multilines text in Markdown format as a table contains current prices.
1345        """
1346        infoText = ""
1347
1348        if show or self.pricesFile:
1349            info = [
1350                "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1351                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1352                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1353            ]
1354
1355            for item in iList:
1356                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1357                    item["ticker"],
1358                    item["figi"],
1359                    item["type"],
1360                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1361                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1362                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1363                    "{} / {}".format(
1364                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1365                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1366                    ),
1367                    "{} / {}".format(
1368                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1369                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1370                    ),
1371                    item["currency"],
1372                ))
1373
1374            infoText = "".join(info)
1375
1376            if show:
1377                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1378
1379            if self.pricesFile:
1380                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1381                    fH.write(infoText)
1382
1383                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1384
1385        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1387    def RequestTradingStatus(self) -> dict:
1388        """
1389        Requesting trading status for the instrument defined by `figi` variable.
1390
1391        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1392
1393        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1394
1395        :return: dictionary with trading status attributes. Response example:
1396                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1397                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1398        """
1399        if self.figi is None or not self.figi:
1400            uLogger.error("Variable `figi` must be defined for using this method!")
1401            raise Exception("FIGI required")
1402
1403        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi))
1404
1405        self.body = str({"figi": self.figi, "instrumentId": self.figi})
1406        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1407        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1408
1409        if self.moreDebug:
1410            uLogger.debug("Records about current trading status successfully received")
1411
1412        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1414    def RequestPortfolio(self) -> dict:
1415        """
1416        Requesting actual user's portfolio for current `accountId`.
1417
1418        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1419
1420        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1421
1422        :return: dictionary with user's portfolio.
1423        """
1424        if self.accountId is None or not self.accountId:
1425            uLogger.error("Variable `accountId` must be defined for using this method!")
1426            raise Exception("Account ID required")
1427
1428        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1429
1430        self.body = str({"accountId": self.accountId})
1431        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1432        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1433
1434        if self.moreDebug:
1435            uLogger.debug("Records about user's portfolio successfully received")
1436
1437        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1439    def RequestPositions(self) -> dict:
1440        """
1441        Requesting open positions by currencies and instruments for current `accountId`.
1442
1443        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1444
1445        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1446
1447        :return: dictionary with open positions by instruments.
1448        """
1449        if self.accountId is None or not self.accountId:
1450            uLogger.error("Variable `accountId` must be defined for using this method!")
1451            raise Exception("Account ID required")
1452
1453        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1454
1455        self.body = str({"accountId": self.accountId})
1456        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1457        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1458
1459        if self.moreDebug:
1460            uLogger.debug("Records about current open positions successfully received")
1461
1462        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1464    def RequestPendingOrders(self) -> list:
1465        """
1466        Requesting current actual pending orders for current `accountId`.
1467
1468        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1469
1470        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1471
1472        :return: list of dictionaries with pending orders.
1473        """
1474        if self.accountId is None or not self.accountId:
1475            uLogger.error("Variable `accountId` must be defined for using this method!")
1476            raise Exception("Account ID required")
1477
1478        uLogger.debug("Requesting current actual pending orders. Wait, please...")
1479
1480        self.body = str({"accountId": self.accountId})
1481        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1482        rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"]
1483
1484        uLogger.debug("[{}] records about pending orders received".format(len(rawOrders)))
1485
1486        return rawOrders

Requesting current actual pending orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending orders.

def RequestStopOrders(self) -> list:
1488    def RequestStopOrders(self) -> list:
1489        """
1490        Requesting current actual stop orders for current `accountId`.
1491
1492        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1493
1494        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1495
1496        :return: list of dictionaries with stop orders.
1497        """
1498        if self.accountId is None or not self.accountId:
1499            uLogger.error("Variable `accountId` must be defined for using this method!")
1500            raise Exception("Account ID required")
1501
1502        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1503
1504        self.body = str({"accountId": self.accountId})
1505        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1506        rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"]
1507
1508        uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1509
1510        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1512    def Overview(self, show: bool = False, details: str = "full") -> dict:
1513        """
1514        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1515        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1516        and `overviewBondsCalendarFile` are defined then also save information to file.
1517
1518        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1519        many requests about the state of the portfolio, and then, based on the received data, a large number
1520        of calculation and statistics are collected.
1521
1522        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1523        :param details: how detailed should the information be?
1524        - `full` — shows full available information about portfolio status (by default),
1525        - `positions` — shows only open positions,
1526        - `orders` — shows only sections of open limits and stop orders.
1527        - `digest` — show a short digest of the portfolio status,
1528        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1529        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1530        :return: dictionary with client's raw portfolio and some statistics.
1531        """
1532        if self.accountId is None or not self.accountId:
1533            uLogger.error("Variable `accountId` must be defined for using this method!")
1534            raise Exception("Account ID required")
1535
1536        view = {
1537            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1538                "headers": {},  # list of dictionaries, response headers without "positions" section
1539                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1540                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1541                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1542                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1543                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1544                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1545                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1546                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1547                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1548            },
1549            "stat": {  # --- some statistics calculated using "raw" sections:
1550                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1551                "availableRUB": 0.,  # available rubles (without other currencies)
1552                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1553                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1554                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1555                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1556                "sharesCostRUB": 0.,  # costs of all shares in RUB
1557                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1558                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1559                "futuresCostRUB": 0.,  # costs of all futures in RUB
1560                "Currencies": [],  # list of dictionaries of all currencies statistics
1561                "Shares": [],  # list of dictionaries of all shares statistics
1562                "Bonds": [],  # list of dictionaries of all bonds statistics
1563                "Etfs": [],  # list of dictionaries of all etfs statistics
1564                "Futures": [],  # list of dictionaries of all futures statistics
1565                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1566                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1567                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1568                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1569                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1570            },
1571            "analytics": {  # --- some analytics of portfolio:
1572                "distrByAssets": {},  # portfolio distribution by assets
1573                "distrByCompanies": {},  # portfolio distribution by companies
1574                "distrBySectors": {},  # portfolio distribution by sectors
1575                "distrByCurrencies": {},  # portfolio distribution by currencies
1576                "distrByCountries": {},  # portfolio distribution by countries
1577                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1578            }
1579        }
1580
1581        details = details.lower()
1582        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1583        if details not in availableDetails:
1584            details = "full"
1585            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1586
1587        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1588
1589        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1590        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1591        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending orders (list)
1592        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1593
1594        # save response headers without "positions" section:
1595        for key in portfolioResponse.keys():
1596            if key != "positions":
1597                view["raw"]["headers"][key] = portfolioResponse[key]
1598
1599            else:
1600                continue
1601
1602        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1603        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1604        for item in portfolioResponse["positions"]:
1605            if item["instrumentType"] == "currency":
1606                self.figi = item["figi"]
1607                curr = self.SearchByFIGI(requestPrice=False)
1608
1609                # current price of currency in RUB:
1610                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1611                    "name": curr["name"],
1612                    "currentPrice": NanoToFloat(
1613                        item["currentPrice"]["units"],
1614                        item["currentPrice"]["nano"]
1615                    ),
1616                }
1617
1618                view["raw"]["Currencies"].append(item)
1619
1620            elif item["instrumentType"] == "share":
1621                view["raw"]["Shares"].append(item)
1622
1623            elif item["instrumentType"] == "bond":
1624                view["raw"]["Bonds"].append(item)
1625
1626            elif item["instrumentType"] == "etf":
1627                view["raw"]["Etfs"].append(item)
1628
1629            elif item["instrumentType"] == "futures":
1630                view["raw"]["Futures"].append(item)
1631
1632            else:
1633                continue
1634
1635        # how many volume of currencies (by ISO currency name) are blocked:
1636        for item in view["raw"]["positions"]["blocked"]:
1637            blocked = NanoToFloat(item["units"], item["nano"])
1638            if blocked > 0:
1639                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1640
1641        # how many volume of instruments (by FIGI) are blocked:
1642        for item in view["raw"]["positions"]["securities"]:
1643            blocked = int(item["blocked"])
1644            if blocked > 0:
1645                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1646
1647        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1648
1649        if "rub" in allBlocked.keys():
1650            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1651
1652        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1653        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1654        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1655        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1656        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1657        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1658        view["stat"]["portfolioCostRUB"] = sum([
1659            view["stat"]["allCurrenciesCostRUB"],
1660            view["stat"]["sharesCostRUB"],
1661            view["stat"]["bondsCostRUB"],
1662            view["stat"]["etfsCostRUB"],
1663            view["stat"]["futuresCostRUB"],
1664        ])
1665
1666        # --- calculating some portfolio statistics:
1667        byComp = {}  # distribution by companies
1668        bySect = {}  # distribution by sectors
1669        byCurr = {}  # distribution by currencies (include RUB)
1670        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1671        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1672
1673        for item in portfolioResponse["positions"]:
1674            self.figi = item["figi"]
1675            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1676
1677            if instrument:
1678                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1679                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1680
1681                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1682                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1683
1684                else:
1685                    blocked = 0
1686
1687                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1688                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1689                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1690                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1691                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1692                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1693                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1694                cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1695                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1696                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1697                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1698                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1699
1700                statData = {
1701                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1702                    "ticker": instrument["ticker"],  # ticker by FIGI
1703                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1704                    "volume": volume,  # available volume of instrument
1705                    "lots": lots,  # volume in lots of instrument
1706                    "direction": direction,  # direction of an instrument's position: short or long
1707                    "blocked": blocked,  # blocked volume of currency or instrument
1708                    "currentPrice": curPrice,  # current instrument's price in basic asset
1709                    "average": average,  # current average position price
1710                    "cost": cost,  # current cost of all volume of instrument in basic asset
1711                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1712                    "costRUB": costRUB,  # cost of instrument in ruble
1713                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1714                    "profit": profit,  # expected profit at current moment
1715                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1716                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1717                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1718                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1719                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1720                    "step": instrument["step"],  # minimum price increment
1721                }
1722
1723                # adding distribution by unique countries:
1724                if statData["country"] not in byCountry.keys():
1725                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1726
1727                else:
1728                    byCountry[statData["country"]]["cost"] += costRUB
1729                    byCountry[statData["country"]]["percent"] += percentCostRUB
1730
1731                if item["instrumentType"] != "currency":
1732                    # adding distribution by unique companies:
1733                    if statData["name"]:
1734                        if statData["name"] not in byComp.keys():
1735                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1736
1737                        else:
1738                            byComp[statData["name"]]["cost"] += costRUB
1739                            byComp[statData["name"]]["percent"] += percentCostRUB
1740
1741                    # adding distribution by unique sectors:
1742                    if statData["sector"] not in bySect.keys():
1743                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1744
1745                    else:
1746                        bySect[statData["sector"]]["cost"] += costRUB
1747                        bySect[statData["sector"]]["percent"] += percentCostRUB
1748
1749                # adding distribution by unique currencies:
1750                if currency not in byCurr.keys():
1751                    byCurr[currency] = {
1752                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1753                        "cost": costRUB,
1754                        "percent": percentCostRUB
1755                    }
1756
1757                else:
1758                    byCurr[currency]["cost"] += costRUB
1759                    byCurr[currency]["percent"] += percentCostRUB
1760
1761                # saving statistics for every instrument:
1762                if item["instrumentType"] == "currency":
1763                    view["stat"]["Currencies"].append(statData)
1764
1765                    # update dict with free funds for trading (total - blocked) by currencies
1766                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1767                    view["stat"]["funds"][currency] = {
1768                        "total": volume,
1769                        "totalCostRUB": costRUB,  # total volume cost in rubles
1770                        "free": volume - blocked,
1771                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1772                    }
1773
1774                elif item["instrumentType"] == "share":
1775                    view["stat"]["Shares"].append(statData)
1776
1777                elif item["instrumentType"] == "bond":
1778                    view["stat"]["Bonds"].append(statData)
1779
1780                elif item["instrumentType"] == "etf":
1781                    view["stat"]["Etfs"].append(statData)
1782
1783                elif item["instrumentType"] == "Futures":
1784                    view["stat"]["Futures"].append(statData)
1785
1786                else:
1787                    continue
1788
1789        # total changes in Russian Ruble:
1790        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1791        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1792        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1793        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1794        view["stat"]["funds"]["rub"] = {
1795            "total": view["stat"]["availableRUB"],
1796            "totalCostRUB": view["stat"]["availableRUB"],
1797            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1798            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1799        }
1800
1801        # --- pending orders sector data:
1802        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending orders to avoid many times price requests
1803        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1804
1805        for item in view["raw"]["orders"]:
1806            self.figi = item["figi"]
1807
1808            if item["figi"] not in uniquePendingOrdersFIGIs:
1809                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1810
1811                uniquePendingOrdersFIGIs.append(item["figi"])
1812                uniquePendingOrders[item["figi"]] = instrument
1813
1814            else:
1815                instrument = uniquePendingOrders[item["figi"]]
1816
1817            if instrument:
1818                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1819                orderType = TKS_ORDER_TYPES[item["orderType"]]
1820                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1821                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1822
1823                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1824                if item["direction"] == "ORDER_DIRECTION_BUY":
1825                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1826
1827                else:
1828                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1829
1830                # requested price for order execution:
1831                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1832
1833                # necessary changes in percent to reach target from current price:
1834                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1835
1836                view["stat"]["orders"].append({
1837                    "orderID": item["orderId"],  # orderId number parameter of current order
1838                    "figi": item["figi"],  # FIGI identification
1839                    "ticker": instrument["ticker"],  # ticker name by FIGI
1840                    "lotsRequested": item["lotsRequested"],  # requested lots value
1841                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1842                    "currentPrice": lastPrice,  # current instrument's price for defined action
1843                    "targetPrice": target,  # requested price for order execution in base currency
1844                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1845                    "percentChanges": changes,  # changes in percent to target from current price
1846                    "currency": item["currency"],  # instrument's currency name
1847                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1848                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1849                    "status": orderState,  # order status from TKS_ORDER_STATES
1850                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1851                })
1852
1853        # --- stop orders sector data:
1854        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1855        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1856
1857        for item in view["raw"]["stopOrders"]:
1858            self.figi = item["figi"]
1859
1860            if item["figi"] not in uniqueStopOrdersFIGIs:
1861                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1862
1863                uniqueStopOrdersFIGIs.append(item["figi"])
1864                uniqueStopOrders[item["figi"]] = instrument
1865
1866            else:
1867                instrument = uniqueStopOrders[item["figi"]]
1868
1869            if instrument:
1870                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1871                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1872                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1873
1874                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1875                if "expirationTime" in item.keys():
1876                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1877                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1878
1879                else:
1880                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1881                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1882
1883                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1884                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1885                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1886
1887                else:
1888                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1889
1890                # requested price when stop-order executed:
1891                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
1892
1893                # price for limit-order, set up when stop-order executed:
1894                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
1895
1896                # necessary changes in percent to reach target from current price:
1897                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1898
1899                view["stat"]["stopOrders"].append({
1900                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
1901                    "figi": item["figi"],  # FIGI identification
1902                    "ticker": instrument["ticker"],  # ticker name by FIGI
1903                    "lotsRequested": item["lotsRequested"],  # requested lots value
1904                    "currentPrice": lastPrice,  # current instrument's price for defined action
1905                    "targetPrice": target,  # requested price for stop-order execution in base currency
1906                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
1907                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
1908                    "percentChanges": changes,  # changes in percent to target from current price
1909                    "currency": item["currency"],  # instrument's currency name
1910                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
1911                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
1912                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
1913                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
1914                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
1915                })
1916
1917        # --- calculating data for analytics section:
1918        # portfolio distribution by assets:
1919        view["analytics"]["distrByAssets"] = {
1920            "Ruble": {
1921                "uniques": 1,
1922                "cost": view["stat"]["availableRUB"],
1923                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1924            },
1925            "Currencies": {
1926                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
1927                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
1928                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1929            },
1930            "Shares": {
1931                "uniques": len(view["stat"]["Shares"]),
1932                "cost": view["stat"]["sharesCostRUB"],
1933                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1934            },
1935            "Bonds": {
1936                "uniques": len(view["stat"]["Bonds"]),
1937                "cost": view["stat"]["bondsCostRUB"],
1938                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1939            },
1940            "Etfs": {
1941                "uniques": len(view["stat"]["Etfs"]),
1942                "cost": view["stat"]["etfsCostRUB"],
1943                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1944            },
1945            "Futures": {
1946                "uniques": len(view["stat"]["Futures"]),
1947                "cost": view["stat"]["futuresCostRUB"],
1948                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1949            },
1950        }
1951
1952        # portfolio distribution by companies:
1953        view["analytics"]["distrByCompanies"]["All money cash"] = {
1954            "ticker": "",
1955            "cost": view["stat"]["allCurrenciesCostRUB"],
1956            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
1957        }
1958        view["analytics"]["distrByCompanies"].update(byComp)
1959
1960        # portfolio distribution by sectors:
1961        view["analytics"]["distrBySectors"]["All money cash"] = {
1962            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
1963            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
1964        }
1965        view["analytics"]["distrBySectors"].update(bySect)
1966
1967        # portfolio distribution by currencies:
1968        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
1969            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
1970
1971            if self.moreDebug:
1972                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
1973
1974        view["analytics"]["distrByCurrencies"].update(byCurr)
1975        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1976        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1977
1978        # portfolio distribution by countries:
1979        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
1980            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
1981
1982            if self.moreDebug:
1983                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
1984
1985        view["analytics"]["distrByCountries"].update(byCountry)
1986        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
1987        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
1988
1989        # --- Prepare text statistics overview in human-readable:
1990        if show:
1991            # Whatever the value `details`, header not changes:
1992            info = [
1993                "# Client's portfolio\n\n",
1994                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1995                "* **Account ID:** [{}]\n".format(self.accountId),
1996            ]
1997
1998            if details in ["full", "positions", "digest"]:
1999                info.extend([
2000                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2001                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2002                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2003                        view["stat"]["totalChangesRUB"],
2004                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2005                        view["stat"]["totalChangesPercentRUB"],
2006                    ),
2007                ])
2008
2009            if details in ["full", "positions"]:
2010                info.extend([
2011                    "## Open positions\n\n",
2012                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2013                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2014                    "| Ruble                       | {:>31} |          |              |              |                     |                              |\n".format(
2015                        "{:.2f} ({:.2f}) rub".format(
2016                            view["stat"]["availableRUB"],
2017                            view["stat"]["blockedRUB"],
2018                        )
2019                    )
2020                ])
2021
2022                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2023                    return [
2024                        "|                             |                                 |          |              |              |                     |                              |\n",
2025                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2026                            noTradeStr if noTradeStr else typeStr,
2027                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2028                        ),
2029                    ]
2030
2031                def _InfoStr(data: dict, showCurrencyName: bool = False) -> str:
2032                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2033                        "{} [{}]".format(data["ticker"], data["figi"]),
2034                        "{:.2f} ({:.2f}) {}".format(
2035                            data["volume"],
2036                            data["blocked"],
2037                            data["currency"],
2038                        ) if showCurrencyName else "{:.0f} ({:.0f})".format(
2039                            data["volume"],
2040                            data["blocked"],
2041                        ),
2042                        "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]),
2043                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2044                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2045                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2046                        "{}{:.2f} {} ({}{:.2f}%)".format(
2047                            "+" if data["profit"] > 0 else "",
2048                            data["profit"], data["baseCurrencyName"],
2049                            "+" if data["percentProfit"] > 0 else "",
2050                            data["percentProfit"],
2051                        ),
2052                    )
2053
2054                # --- Show currencies section:
2055                if view["stat"]["Currencies"]:
2056                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2057                    for item in view["stat"]["Currencies"]:
2058                        info.append(_InfoStr(item, showCurrencyName=True))
2059
2060                else:
2061                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2062
2063                # --- Show shares section:
2064                if view["stat"]["Shares"]:
2065                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2066
2067                    for item in view["stat"]["Shares"]:
2068                        info.append(_InfoStr(item))
2069
2070                else:
2071                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2072
2073                # --- Show bonds section:
2074                if view["stat"]["Bonds"]:
2075                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2076
2077                    for item in view["stat"]["Bonds"]:
2078                        info.append(_InfoStr(item))
2079
2080                else:
2081                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2082
2083                # --- Show etfs section:
2084                if view["stat"]["Etfs"]:
2085                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2086
2087                    for item in view["stat"]["Etfs"]:
2088                        info.append(_InfoStr(item))
2089
2090                else:
2091                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2092
2093                # --- Show futures section:
2094                if view["stat"]["Futures"]:
2095                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2096
2097                    for item in view["stat"]["Futures"]:
2098                        info.append(_InfoStr(item))
2099
2100                else:
2101                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2102
2103            if details in ["full", "orders"]:
2104                # --- Show pending orders section:
2105                if view["stat"]["orders"]:
2106                    info.extend([
2107                        "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])),
2108                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2109                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2110                    ])
2111
2112                    for item in view["stat"]["orders"]:
2113                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2114                            "{} [{}]".format(item["ticker"], item["figi"]),
2115                            item["orderID"],
2116                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2117                            "{} {} ({}{:.2f}%)".format(
2118                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2119                                item["baseCurrencyName"],
2120                                "+" if item["percentChanges"] > 0 else "",
2121                                float(item["percentChanges"]),
2122                            ),
2123                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2124                            item["action"],
2125                            item["type"],
2126                            item["date"],
2127                        ))
2128
2129                else:
2130                    info.append("\n## Total pending limit-orders: 0\n")
2131
2132                # --- Show stop orders section:
2133                if view["stat"]["stopOrders"]:
2134                    info.extend([
2135                        "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])),
2136                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2137                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2138                    ])
2139
2140                    for item in view["stat"]["stopOrders"]:
2141                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2142                            "{} [{}]".format(item["ticker"], item["figi"]),
2143                            item["orderID"],
2144                            item["lotsRequested"],
2145                            "{} {} ({}{:.2f}%)".format(
2146                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2147                                item["baseCurrencyName"],
2148                                "+" if item["percentChanges"] > 0 else "",
2149                                float(item["percentChanges"]),
2150                            ),
2151                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2152                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2153                            item["action"],
2154                            item["type"],
2155                            item["expType"],
2156                            item["createDate"],
2157                            item["expDate"],
2158                        ))
2159
2160                else:
2161                    info.append("\n## Total stop-orders: 0\n")
2162
2163            if details in ["full", "analytics"]:
2164                # -- Show analytics section:
2165                if view["stat"]["portfolioCostRUB"] > 0:
2166                    info.extend([
2167                        "\n# Analytics\n"
2168                        "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2169                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2170                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2171                            view["stat"]["totalChangesRUB"],
2172                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2173                            view["stat"]["totalChangesPercentRUB"],
2174                        ),
2175                        "\n## Portfolio distribution by assets\n"
2176                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2177                        "|------------------------------------|---------|---------|--------------------|\n",
2178                    ])
2179
2180                    for key in view["analytics"]["distrByAssets"].keys():
2181                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2182                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2183                                key,
2184                                view["analytics"]["distrByAssets"][key]["uniques"],
2185                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2186                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2187                            ))
2188
2189                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2190
2191                    info.extend([
2192                        "\n## Portfolio distribution by companies\n"
2193                        "\n| Company                                      | Percent | Current cost       |\n",
2194                        aSepLine,
2195                    ])
2196
2197                    for company in view["analytics"]["distrByCompanies"].keys():
2198                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2199                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2200                                "{}{}".format(
2201                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2202                                    company,
2203                                ),
2204                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2205                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2206                            ))
2207
2208                    info.extend([
2209                        "\n## Portfolio distribution by sectors\n"
2210                        "\n| Sector                                       | Percent | Current cost       |\n",
2211                        aSepLine,
2212                    ])
2213
2214                    for sector in view["analytics"]["distrBySectors"].keys():
2215                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2216                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2217                                sector,
2218                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2219                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2220                            ))
2221
2222                    info.extend([
2223                        "\n## Portfolio distribution by currencies\n"
2224                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2225                        aSepLine,
2226                    ])
2227
2228                    for curr in view["analytics"]["distrByCurrencies"].keys():
2229                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2230                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2231                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2232                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2233                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2234                            ))
2235
2236                    info.extend([
2237                        "\n## Portfolio distribution by countries\n"
2238                        "\n| Assets by country                            | Percent | Current cost       |\n",
2239                        aSepLine,
2240                    ])
2241
2242                    for country in view["analytics"]["distrByCountries"].keys():
2243                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2244                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2245                                country,
2246                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2247                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2248                            ))
2249
2250            if details in ["full", "calendar"]:
2251                # -- Show bonds payment calendar section:
2252                if view["stat"]["Bonds"]:
2253                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2254                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2255                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2256
2257                else:
2258                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2259
2260            infoText = "".join(info)
2261
2262            uLogger.info(infoText)
2263
2264            if details == "full" and self.overviewFile:
2265                filename = self.overviewFile
2266
2267            elif details == "digest" and self.overviewDigestFile:
2268                filename = self.overviewDigestFile
2269
2270            elif details == "positions" and self.overviewPositionsFile:
2271                filename = self.overviewPositionsFile
2272
2273            elif details == "orders" and self.overviewOrdersFile:
2274                filename = self.overviewOrdersFile
2275
2276            elif details == "analytics" and self.overviewAnalyticsFile:
2277                filename = self.overviewAnalyticsFile
2278
2279            elif details == "calendar" and self.overviewBondsCalendarFile:
2280                filename = self.overviewBondsCalendarFile
2281
2282            else:
2283                filename = ""
2284
2285            if filename:
2286                with open(filename, "w", encoding="UTF-8") as fH:
2287                    fH.write(infoText)
2288
2289                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2290
2291        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2293    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2294        """
2295        Returns history operations between two given dates for current `accountId`.
2296        If `reportFile` string is not empty then also save human-readable report.
2297        Shows some statistical data of closed positions.
2298
2299        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2300        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2301        :param show: if `True` then also prints all records to the console.
2302        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2303        :return: original list of dictionaries with history of deals records from API ("operations" key):
2304                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2305                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2306        """
2307        if self.accountId is None or not self.accountId:
2308            uLogger.error("Variable `accountId` must be defined for using this method!")
2309            raise Exception("Account ID required")
2310
2311        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2312
2313        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2314
2315        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2316        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2317        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2318        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2319        customStat = {}  # custom statistics in additional to responseJSON
2320
2321        # --- output report in human-readable format:
2322        if show or self.reportFile:
2323            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2324            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2325            nextDay = ""
2326
2327            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2328
2329            if len(ops) > 0:
2330                customStat = {
2331                    "opsCount": 0,  # total operations count
2332                    "buyCount": 0,  # buy operations
2333                    "sellCount": 0,  # sell operations
2334                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2335                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2336                    "payIn": {"rub": 0.},  # Deposit brokerage account
2337                    "payOut": {"rub": 0.},  # Withdrawals
2338                    "divs": {"rub": 0.},  # Dividends income
2339                    "coupons": {"rub": 0.},  # Coupon's income
2340                    "brokerCom": {"rub": 0.},  # Service commissions
2341                    "serviceCom": {"rub": 0.},  # Service commissions
2342                    "marginCom": {"rub": 0.},  # Margin commissions
2343                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2344                }
2345
2346                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2347                for item in ops:
2348                    if item["state"] == "OPERATION_STATE_EXECUTED":
2349                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2350
2351                        # count buy operations:
2352                        if "_BUY" in item["operationType"]:
2353                            customStat["buyCount"] += 1
2354
2355                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2356                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2357
2358                            else:
2359                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2360
2361                        # count sell operations:
2362                        elif "_SELL" in item["operationType"]:
2363                            customStat["sellCount"] += 1
2364
2365                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2366                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2367
2368                            else:
2369                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2370
2371                        # count incoming operations:
2372                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2373                            if item["payment"]["currency"] in customStat["payIn"].keys():
2374                                customStat["payIn"][item["payment"]["currency"]] += payment
2375
2376                            else:
2377                                customStat["payIn"][item["payment"]["currency"]] = payment
2378
2379                        # count withdrawals operations:
2380                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2381                            if item["payment"]["currency"] in customStat["payOut"].keys():
2382                                customStat["payOut"][item["payment"]["currency"]] += payment
2383
2384                            else:
2385                                customStat["payOut"][item["payment"]["currency"]] = payment
2386
2387                        # count dividends income:
2388                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2389                            if item["payment"]["currency"] in customStat["divs"].keys():
2390                                customStat["divs"][item["payment"]["currency"]] += payment
2391
2392                            else:
2393                                customStat["divs"][item["payment"]["currency"]] = payment
2394
2395                        # count coupon's income:
2396                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2397                            if item["payment"]["currency"] in customStat["coupons"].keys():
2398                                customStat["coupons"][item["payment"]["currency"]] += payment
2399
2400                            else:
2401                                customStat["coupons"][item["payment"]["currency"]] = payment
2402
2403                        # count broker commissions:
2404                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2405                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2406                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2407
2408                            else:
2409                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2410
2411                        # count service commissions:
2412                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2413                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2414                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2415
2416                            else:
2417                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2418
2419                        # count margin commissions:
2420                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2421                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2422                                customStat["marginCom"][item["payment"]["currency"]] += payment
2423
2424                            else:
2425                                customStat["marginCom"][item["payment"]["currency"]] = payment
2426
2427                        # count withholding taxes:
2428                        elif "_TAX" in item["operationType"]:
2429                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2430                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2431
2432                            else:
2433                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2434
2435                        else:
2436                            continue
2437
2438                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2439
2440                # --- view "Actions" lines:
2441                info.extend([
2442                    "| Report sections            |                               |                              |                      |                        |\n",
2443                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2444                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2445                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2446                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2447                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2448                    ),
2449                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2450                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2451                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2452                    ),
2453                ])
2454
2455                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2456                for key in opsKeys:
2457                    if key == "rub":
2458                        continue
2459
2460                    info.extend([
2461                        "|                            |                               | {:<28} |                      |                        |\n".format(
2462                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2463                        ),
2464                        "|                            |                               | {:<28} |                      |                        |\n".format(
2465                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2466                        ),
2467                    ])
2468
2469                info.append(splitLine1)
2470
2471                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2472                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2473                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2474                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2475                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2476                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2477                    )
2478
2479                # --- view "Payments" lines:
2480                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2481                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2482
2483                for key in paymentsKeys:
2484                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2485
2486                info.append(splitLine1)
2487
2488                # --- view "Commissions and taxes" lines:
2489                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2490                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2491
2492                for key in comKeys:
2493                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2494
2495                info.append(splitLine1)
2496
2497                info.extend([
2498                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2499                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2500                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2501                ])
2502
2503            else:
2504                info.append("Broker returned no operations during this period\n")
2505
2506            # --- view "Operations" section:
2507            for item in ops:
2508                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2509                    continue
2510
2511                else:
2512                    self.figi = item["figi"] if item["figi"] else ""
2513                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2514                    instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {}
2515
2516                    # group of deals during one day:
2517                    if nextDay and item["date"].split("T")[0] != nextDay:
2518                        info.append(splitLine2)
2519                        nextDay = ""
2520
2521                    else:
2522                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2523
2524                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2525                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2526                        self.figi if self.figi else "—",
2527                        instrument["ticker"] if instrument else "—",
2528                        instrument["type"] if instrument else "—",
2529                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2530                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2531                        TKS_OPERATION_STATES[item["state"]],
2532                        TKS_OPERATION_TYPES[item["operationType"]],
2533                    ))
2534
2535            infoText = "".join(info)
2536
2537            if show:
2538                if self.moreDebug:
2539                    uLogger.debug("Records about history of a client's operations successfully received")
2540
2541                uLogger.info(infoText)
2542
2543            if self.reportFile:
2544                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2545                    fH.write(infoText)
2546
2547                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2548
2549        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2551    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2552        """
2553        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2554
2555        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2556        Warning! Broker server used ISO UTC time by default.
2557
2558        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2559        Also, `historyFile` used to update history with `onlyMissing` parameter.
2560
2561        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2562
2563        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2564        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2565        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2566                         `"hour"`, `"day"`. Default: `"hour"`.
2567        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2568                            False by default. Warning! History appends only from last candle to current time
2569                            with always update last candle!
2570        :param csvSep: separator if csv-file is used, `,` by default.
2571        :param show: if `True` then also prints Pandas DataFrame to the console.
2572        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2573                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2574        """
2575        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2576        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2577        history = None  # empty pandas object for history
2578
2579        if interval not in TKS_CANDLE_INTERVALS.keys():
2580            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2581            raise Exception("Incorrect value")
2582
2583        if not (self.ticker or self.figi):
2584            uLogger.error("Ticker or FIGI must be defined!")
2585            raise Exception("Ticker or FIGI required")
2586
2587        if self.ticker and not self.figi:
2588            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2589            self.figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2590
2591        if self.figi and not self.ticker:
2592            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2593            self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2594
2595        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2596        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2597        if interval.lower() != "day":
2598            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2599
2600        delta = dtEnd - dtStart  # current UTC time minus last time in file
2601        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2602
2603        # calculate history length in candles:
2604        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2605        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2606            length += 1  # to avoid fraction time
2607
2608        # calculate data blocks count:
2609        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2610
2611        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2612        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2613        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2614        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2615        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi))
2616
2617        tempOld = None  # pandas object for old history, if --only-missing key present
2618        lastTime = None  # datetime object of last old candle in file
2619
2620        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2621            uLogger.debug("--only-missing key present, add only last missing candles...")
2622            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2623
2624            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2625
2626            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2627            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2628            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2629            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2630
2631            # get last datetime object from last string in file or minus 1 delta if file is empty:
2632            if len(tempOld) > 0:
2633                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2634
2635            else:
2636                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2637
2638            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2639
2640        responseJSONs = []  # raw history blocks of data
2641
2642        blockEnd = dtEnd
2643        for item in range(blocks):
2644            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2645            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2646
2647            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2648                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2649            ))
2650
2651            if blockStart == blockEnd:
2652                uLogger.debug("Skipped this zero-length block...")
2653
2654            else:
2655                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2656                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2657                self.body = str({
2658                    "figi": self.figi,
2659                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2660                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2661                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2662                })
2663                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2664
2665                if "code" in responseJSON.keys():
2666                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2667
2668                else:
2669                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2670                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2671
2672                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2673
2674            blockEnd = blockStart
2675
2676        printCount = len(responseJSONs)  # candles to show in console
2677        if responseJSONs:
2678            tempHistory = pd.DataFrame(
2679                data={
2680                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2681                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2682                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2683                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2684                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2685                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2686                    "volume": [int(item["volume"]) for item in responseJSONs],
2687                },
2688                index=range(len(responseJSONs)),
2689                columns=["date", "time", "open", "high", "low", "close", "volume"],
2690            )
2691            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2692            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2693
2694            # append only newest candles to old history if --only-missing key present:
2695            if onlyMissing and tempOld is not None and lastTime is not None:
2696                index = 0  # find start index in tempHistory data:
2697
2698                for i, item in tempHistory.iterrows():
2699                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2700
2701                    if curTime == lastTime:
2702                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2703                        index = i
2704                        printCount = index + 1
2705                        break
2706
2707                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2708
2709            else:
2710                history = tempHistory  # if no `--only-missing` key then load full data from server
2711
2712            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2713
2714        if history is not None and not history.empty:
2715            if show:
2716                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2717                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2718                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2719                ))
2720
2721        else:
2722            uLogger.warning("Received an empty candles history!")
2723
2724        if self.historyFile is not None:
2725            if history is not None and not history.empty:
2726                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2727                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile)))
2728
2729            else:
2730                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2731
2732        else:
2733            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2734
2735        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2737    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2738        """
2739        Load candles history from csv-file and return Pandas DataFrame object.
2740
2741        See also: `History()` and `ShowHistoryChart()` methods.
2742
2743        :param filePath: path to csv-file to open.
2744        """
2745        loadedHistory = None  # init candles data object
2746
2747        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2748
2749        if os.path.exists(filePath):
2750            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2751
2752            tfStr = self.priceModel.FormattedDelta(
2753                self.priceModel.timeframe,
2754                "{days} days {hours}h {minutes}m {seconds}s",
2755            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2756                self.priceModel.timeframe,
2757                "{hours}h {minutes}m {seconds}s",
2758            )
2759
2760            if loadedHistory is not None and not loadedHistory.empty:
2761                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2762                    len(loadedHistory),
2763                    tfStr,
2764                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2765                )
2766
2767            else:
2768                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2769
2770        else:
2771            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2772
2773        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2775    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2776        """
2777        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2778
2779        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2780        Default: `index.html` (both for interact and non-interact candlesticks chart).
2781
2782        See also: `History()` and `LoadHistory()` methods.
2783
2784        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2785        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2786                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2787                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2788                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2789        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2790                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2791        """
2792        if isinstance(candles, str):
2793            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2794            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2795
2796        elif isinstance(candles, pd.DataFrame):
2797            self.priceModel.prices = candles  # set candles chain from variable
2798            self.priceModel.ticker = self.ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2799
2800            if "datetime" not in candles.columns:
2801                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2802
2803        else:
2804            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2805            raise Exception("Incorrect value")
2806
2807        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2808
2809        if interact:
2810            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2811
2812            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2813
2814        else:
2815            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2816
2817            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2818
2819        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2821    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2822        """
2823        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2824        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2825
2826        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2827
2828        :param operation: string "Buy" or "Sell".
2829        :param lots: volume, integer count of lots >= 1.
2830        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2831        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2832        :param expDate: string "Undefined" by default or local date in future,
2833                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2834        :return: JSON with response from broker server.
2835        """
2836        if self.accountId is None or not self.accountId:
2837            uLogger.error("Variable `accountId` must be defined for using this method!")
2838            raise Exception("Account ID required")
2839
2840        if operation is None or not operation or operation not in ("Buy", "Sell"):
2841            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2842            raise Exception("Incorrect value")
2843
2844        if lots is None or lots < 1:
2845            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2846            lots = 1
2847
2848        if tp is None or tp < 0:
2849            tp = 0
2850
2851        if sl is None or sl < 0:
2852            sl = 0
2853
2854        if expDate is None or not expDate:
2855            expDate = "Undefined"
2856
2857        if not (self.ticker or self.figi):
2858            uLogger.error("Ticker or FIGI must be defined!")
2859            raise Exception("Ticker or FIGI required")
2860
2861        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
2862        self.ticker = instrument["ticker"]
2863        self.figi = instrument["figi"]
2864
2865        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate))
2866
2867        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2868        self.body = str({
2869            "figi": self.figi,
2870            "quantity": str(lots),
2871            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
2872            "accountId": str(self.accountId),
2873            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
2874        })
2875        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
2876
2877        if "orderId" in response.keys():
2878            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
2879                operation, response["orderId"],
2880                self.ticker, self.figi, lots,
2881                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
2882                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
2883                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
2884            ))
2885
2886            if tp > 0:
2887                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
2888
2889            if sl > 0:
2890                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
2891
2892        else:
2893            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.")
2894
2895        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2897    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2898        """
2899        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
2900        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
2901
2902        See also: `Order()` and `Trade()` docstrings.
2903
2904        :param lots: volume, integer count of lots >= 1.
2905        :param tp: float > 0, take profit price of stop-order.
2906        :param sl: float > 0, stop loss price of stop-order.
2907        :param expDate: it's a local date in future.
2908                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2909        :return: JSON with response from broker server.
2910        """
2911        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2913    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2914        """
2915        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
2916        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2917
2918        See also: `Order()` and `Trade()` docstrings.
2919
2920        :param lots: volume, integer count of lots >= 1.
2921        :param tp: float > 0, take profit price of stop-order.
2922        :param sl: float > 0, stop loss price of stop-order.
2923        :param expDate: it's a local date in the future.
2924                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
2925        :return: JSON with response from broker server.
2926        """
2927        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2929    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
2930        """
2931        Close position of given instruments.
2932
2933        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
2934        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
2935                         This avoids unnecessary downloading data from the server.
2936        """
2937        if instruments is None or not instruments:
2938            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
2939            raise Exception("Ticker or FIGI required")
2940
2941        if isinstance(instruments, str):
2942            instruments = [instruments]
2943
2944        uniqueInstruments = self.GetUniqueFIGIs(instruments)
2945        if uniqueInstruments:
2946            if portfolio is None or not portfolio:
2947                portfolio = self.Overview(show=False)
2948
2949            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
2950            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
2951
2952            for self.figi in uniqueInstruments:
2953                if self.figi not in allOpened:
2954                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi))
2955                    continue
2956
2957                # search open trade info about instrument by ticker:
2958                instrument = {}
2959                for iType in TKS_INSTRUMENTS:
2960                    if instrument:
2961                        break
2962
2963                    for item in portfolio["stat"][iType]:
2964                        if item["figi"] == self.figi:
2965                            instrument = item
2966                            break
2967
2968                if instrument:
2969                    self.ticker = instrument["ticker"]
2970                    self.figi = instrument["figi"]
2971
2972                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
2973                        self.ticker,
2974                        self.figi,
2975                        int(instrument["volume"]),
2976                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
2977                    ))
2978
2979                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
2980
2981                    if tradeLots > 0:
2982                        if instrument["blocked"] > 0:
2983                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
2984                                instrument["blocked"],
2985                                self.ticker,
2986                                tradeLots,
2987                            ))
2988
2989                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
2990                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
2991
2992                    else:
2993                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2995    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
2996        """
2997        Close all positions of given instruments with defined type.
2998
2999        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3000        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3001                         This avoids unnecessary downloading data from the server.
3002        """
3003        if iType not in TKS_INSTRUMENTS:
3004            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3005
3006        else:
3007            if portfolio is None or not portfolio:
3008                portfolio = self.Overview(show=False)
3009
3010            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3011            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3012
3013            if tickers and portfolio:
3014                self.CloseTrades(tickers, portfolio)
3015
3016            else:
3017                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3019    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3020        """
3021        Universal method to create market or limit orders with all available parameters for current `accountId`.
3022        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3023
3024        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3025        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3026
3027        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3028        then broker immediately open market order as you can do simple --buy or --sell operations!
3029
3030        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3031        When current price will go up or down to target price value then broker opens a limit order.
3032        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3033
3034        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3035
3036        :param operation: string "Buy" or "Sell".
3037        :param orderType: string "Limit" or "Stop".
3038        :param lots: volume, integer count of lots >= 1.
3039        :param targetPrice: target price > 0. This is open trade price for limit order.
3040        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3041                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3042        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3043                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3044                         Stop loss order always executed by market price.
3045        :param expDate: string "Undefined" by default or local date in future.
3046                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3047                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3048                        A limit order has no expiration date, it lasts until the end of the trading day.
3049        :return: JSON with response from broker server.
3050        """
3051        if self.accountId is None or not self.accountId:
3052            uLogger.error("Variable `accountId` must be defined for using this method!")
3053            raise Exception("Account ID required")
3054
3055        if operation is None or not operation or operation not in ("Buy", "Sell"):
3056            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3057            raise Exception("Incorrect value")
3058
3059        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3060            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3061            raise Exception("Incorrect value")
3062
3063        if lots is None or lots < 1:
3064            uLogger.error("You must define trade volume > 0: integer count of lots!")
3065            raise Exception("Incorrect value")
3066
3067        if targetPrice is None or targetPrice <= 0:
3068            uLogger.error("Target price for limit-order must be greater than 0!")
3069            raise Exception("Incorrect value")
3070
3071        if limitPrice is None or limitPrice <= 0:
3072            limitPrice = targetPrice
3073
3074        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3075            stopType = "Limit"
3076
3077        if expDate is None or not expDate:
3078            expDate = "Undefined"
3079
3080        if not (self.ticker or self.figi):
3081            uLogger.error("Tocker or FIGI must be defined!")
3082            raise Exception("Ticker or FIGI required")
3083
3084        response = {}
3085        instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True)
3086        self.ticker = instrument["ticker"]
3087        self.figi = instrument["figi"]
3088
3089        if orderType == "Limit":
3090            uLogger.debug(
3091                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3092                    self.ticker, self.figi,
3093                    operation, lots, targetPrice, instrument["currency"],
3094                ))
3095
3096            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3097            self.body = str({
3098                "figi": self.figi,
3099                "quantity": str(lots),
3100                "price": FloatToNano(targetPrice),
3101                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3102                "accountId": str(self.accountId),
3103                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3104            })
3105            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3106
3107            if "orderId" in response.keys():
3108                uLogger.info(
3109                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format(
3110                        response["orderId"],
3111                        self.ticker, self.figi,
3112                        operation, lots, targetPrice, instrument["currency"],
3113                    ))
3114
3115                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3116                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3117                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3118                            targetPrice, instrument["currency"],
3119                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3120                        ))
3121
3122                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3123                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3124                            targetPrice, instrument["currency"],
3125                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3126                        ))
3127
3128            else:
3129                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.")
3130
3131        if orderType == "Stop":
3132            uLogger.debug(
3133                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3134                    self.ticker, self.figi,
3135                    operation, lots,
3136                    targetPrice, instrument["currency"],
3137                    limitPrice, instrument["currency"],
3138                    stopType, expDate,
3139                ))
3140
3141            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3142            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3143            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3144
3145            body = {
3146                "figi": self.figi,
3147                "quantity": str(lots),
3148                "price": FloatToNano(limitPrice),
3149                "stopPrice": FloatToNano(targetPrice),
3150                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3151                "accountId": str(self.accountId),
3152                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3153                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3154            }
3155
3156            if expDateUTC:
3157                body["expireDate"] = expDateUTC
3158
3159            self.body = str(body)
3160            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3161
3162            if "stopOrderId" in response.keys():
3163                uLogger.info(
3164                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format(
3165                        response["stopOrderId"],
3166                        self.ticker, self.figi,
3167                        operation, lots,
3168                        targetPrice, instrument["currency"],
3169                        limitPrice, instrument["currency"],
3170                        TKS_STOP_ORDER_TYPES[stopOrderType],
3171                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3172                    ))
3173
3174                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3175                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3176                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3177                            targetPrice, instrument["currency"],
3178                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3179                        ))
3180
3181                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3182                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3183                            targetPrice, instrument["currency"],
3184                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3185                        ))
3186
3187            else:
3188                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.")
3189
3190        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3192    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3193        """
3194        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3195        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3196        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3197        See also: `Order()` docstring.
3198
3199        :param lots: volume, integer count of lots >= 1.
3200        :param targetPrice: target price > 0. This is open trade price for limit order.
3201        :return: JSON with response from broker server.
3202        """
3203        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3205    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3206        """
3207        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3208        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3209        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3210        target price value then broker opens a limit order. See also: `Order()` docstring.
3211
3212        :param lots: volume, integer count of lots >= 1.
3213        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3214        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3215                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3216        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3217                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3218        :param expDate: string "Undefined" by default or local date in future.
3219                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3220                        This date is converting to UTC format for server.
3221        :return: JSON with response from broker server.
3222        """
3223        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3225    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3226        """
3227        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3228        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3229        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3230        See also: `Order()` docstring.
3231
3232        :param lots: volume, integer count of lots >= 1.
3233        :param targetPrice: target price > 0. This is open trade price for limit order.
3234        :return: JSON with response from broker server.
3235        """
3236        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3238    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3239        """
3240        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3241        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3242        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3243        target price value then broker opens a limit order. See also: `Order()` docstring.
3244
3245        :param lots: volume, integer count of lots >= 1.
3246        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3247        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3248                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3249        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3250                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3251        :param expDate: string "Undefined" by default or local date in future.
3252                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3253                        This date is converting to UTC format for server.
3254        :return: JSON with response from broker server.
3255        """
3256        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3258    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3259        """
3260        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3261
3262        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3263        :param allOrdersIDs: pre-received lists of all active pending orders.
3264                             This avoids unnecessary downloading data from the server.
3265        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3266        """
3267        if self.accountId is None or not self.accountId:
3268            uLogger.error("Variable `accountId` must be defined for using this method!")
3269            raise Exception("Account ID required")
3270
3271        if orderIDs:
3272            if allOrdersIDs is None or not allOrdersIDs:
3273                rawOrders = self.RequestPendingOrders()
3274                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3275
3276            if allStopOrdersIDs is None or not allStopOrdersIDs:
3277                rawStopOrders = self.RequestStopOrders()
3278                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3279
3280            for orderID in orderIDs:
3281                idInPendingOrders = orderID in allOrdersIDs
3282                idInStopOrders = orderID in allStopOrdersIDs
3283
3284                if not (idInPendingOrders or idInStopOrders):
3285                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3286                    continue
3287
3288                else:
3289                    if idInPendingOrders:
3290                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3291
3292                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3293                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3294                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3295                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3296
3297                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3298                            if self.moreDebug:
3299                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3300
3301                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3302
3303                        else:
3304                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3305
3306                    elif idInStopOrders:
3307                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3308
3309                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3310                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3311                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3312                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3313
3314                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3315                            if self.moreDebug:
3316                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3317
3318                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3319
3320                        else:
3321                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3322
3323                    else:
3324                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3326    def CloseAllOrders(self) -> None:
3327        """
3328        Gets a list of open pending and stop orders and cancel it all.
3329        """
3330        rawOrders = self.RequestPendingOrders()
3331        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending orders ID
3332        lenOrders = len(allOrdersIDs)
3333
3334        rawStopOrders = self.RequestStopOrders()
3335        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3336        lenSOrders = len(allStopOrdersIDs)
3337
3338        if lenOrders > 0 or lenSOrders > 0:
3339            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3340
3341            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3342
3343        else:
3344            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3346    def CloseAll(self, *args) -> None:
3347        """
3348        Close all available (not blocked) opened trades and orders.
3349
3350        Also, you can select one or more keywords case-insensitive:
3351        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3352
3353        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3354        """
3355        overview = self.Overview(show=False)  # get all open trades info
3356
3357        if len(args) == 0:
3358            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3359            self.CloseAllOrders()  # close all pending and stop orders
3360
3361            for iType in TKS_INSTRUMENTS:
3362                if iType != "Currencies":
3363                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3364
3365        else:
3366            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3367            lowerArgs = [x.lower() for x in args]
3368
3369            if "orders" in lowerArgs:
3370                self.CloseAllOrders()  # close all pending and stop orders
3371
3372            for iType in TKS_INSTRUMENTS:
3373                if iType.lower() in lowerArgs and iType != "Currencies":
3374                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

@staticmethod
def ParseOrderParameters(operation, **inputParameters)
3376    @staticmethod
3377    def ParseOrderParameters(operation, **inputParameters):
3378        """
3379        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3380
3381        :param operation: string "Buy" or "Sell".
3382        :param inputParameters: this is dict of strings that looks like this
3383               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3384               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3385               "prices" key: one or more prices to open limit-orders
3386               Counts of values in lots and prices lists must be equals!
3387        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3388        """
3389        # TODO: update order grid work with api v2
3390        pass
3391        # uLogger.debug("Input parameters: {}".format(inputParameters))
3392        #
3393        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3394        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3395        #     raise Exception("Incorrect value")
3396        #
3397        # if "l" in inputParameters.keys():
3398        #     inputParameters["lots"] = inputParameters.pop("l")
3399        #
3400        # if "p" in inputParameters.keys():
3401        #     inputParameters["prices"] = inputParameters.pop("p")
3402        #
3403        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3404        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3405        #     raise Exception("Incorrect value")
3406        #
3407        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3408        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3409        #
3410        # if len(lots) != len(prices):
3411        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3412        #     raise Exception("Incorrect value")
3413        #
3414        # uLogger.debug("Extracted parameters for orders:")
3415        # uLogger.debug("lots = {}".format(lots))
3416        # uLogger.debug("prices = {}".format(prices))
3417        #
3418        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3419        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3420        # uLogger.debug("Order parameters: {}".format(result))
3421        #
3422        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3424    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3425        """
3426        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3427
3428        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3429        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3430        """
3431        result = False
3432        msg = "Instrument not defined!"
3433
3434        if portfolio is None or not portfolio:
3435            portfolio = self.Overview(show=False)
3436
3437        if self.ticker:
3438            uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker))
3439            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3440
3441            for iType in TKS_INSTRUMENTS:
3442                for instrument in portfolio["stat"][iType]:
3443                    if instrument["ticker"] == self.ticker:
3444                        result = True
3445                        msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker)
3446                        break
3447
3448        elif self.figi:
3449            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3450            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3451
3452            for iType in TKS_INSTRUMENTS:
3453                for instrument in portfolio["stat"][iType]:
3454                    if instrument["figi"] == self.figi:
3455                        result = True
3456                        msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi)
3457                        break
3458
3459        else:
3460            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3461
3462        uLogger.debug(msg)
3463
3464        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3466    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3467        """
3468        Returns instrument from the user's portfolio if it presents there.
3469        Instrument must be defined by `ticker` (highly priority) or `figi`.
3470
3471        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3472        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3473        """
3474        result = None
3475        msg = "Instrument not defined!"
3476
3477        if portfolio is None or not portfolio:
3478            portfolio = self.Overview(show=False)
3479
3480        if self.ticker:
3481            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self.ticker))
3482            msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker)
3483
3484            for iType in TKS_INSTRUMENTS:
3485                for instrument in portfolio["stat"][iType]:
3486                    if instrument["ticker"] == self.ticker:
3487                        result = instrument
3488                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"])
3489                        break
3490
3491        elif self.figi:
3492            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi))
3493            msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi)
3494
3495            for iType in TKS_INSTRUMENTS:
3496                for instrument in portfolio["stat"][iType]:
3497                    if instrument["figi"] == self.figi:
3498                        result = instrument
3499                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi)
3500                        break
3501
3502        else:
3503            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3504
3505        uLogger.debug(msg)
3506
3507        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def RequestLimits(self) -> dict:
3509    def RequestLimits(self) -> dict:
3510        """
3511        Method for obtaining the available funds for withdrawal for current `accountId`.
3512
3513        See also:
3514        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3515        - `OverviewLimits()` method
3516
3517        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3518                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3519                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3520                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3521        """
3522        if self.accountId is None or not self.accountId:
3523            uLogger.error("Variable `accountId` must be defined for using this method!")
3524            raise Exception("Account ID required")
3525
3526        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3527
3528        self.body = str({"accountId": self.accountId})
3529        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3530        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3531
3532        if self.moreDebug:
3533            uLogger.debug("Records about available funds for withdrawal successfully received")
3534
3535        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3537    def OverviewLimits(self, show: bool = False) -> dict:
3538        """
3539        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3540
3541        See also: `RequestLimits()`.
3542
3543        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3544        :return: dict with raw parsed data from server and some calculated statistics about it.
3545        """
3546        if self.accountId is None or not self.accountId:
3547            uLogger.error("Variable `accountId` must be defined for using this method!")
3548            raise Exception("Account ID required")
3549
3550        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3551
3552        view = {
3553            "rawLimits": rawLimits,
3554            "limits": {  # parsed data for every currency:
3555                "money": {  # this is an array of portfolio currency positions
3556                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3557                },
3558                "blocked": {  # this is an array of blocked currency
3559                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3560                },
3561                "blockedGuarantee": {  # this is locked money under collateral for futures
3562                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3563                },
3564            },
3565        }
3566
3567        # --- Prepare text table with limits in human-readable format:
3568        if show:
3569            info = [
3570                "# Withdrawal limits\n\n",
3571                "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3572                "* **Account ID:** [{}]\n".format(self.accountId),
3573            ]
3574
3575            if view["limits"]["money"]:
3576                info.extend([
3577                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3578                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3579                ])
3580
3581            else:
3582                info.append("\nNo withdrawal limits\n")
3583
3584            for curr in view["limits"]["money"].keys():
3585                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3586                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3587                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3588
3589                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3590                    "[{}]".format(curr),
3591                    "{:.2f}".format(view["limits"]["money"][curr]),
3592                    "{:.2f}".format(availableMoney),
3593                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3594                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3595                )
3596
3597                if curr == "rub":
3598                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3599
3600                else:
3601                    info.append(infoStr)
3602
3603            infoText = "".join(info)
3604
3605            uLogger.info(infoText)
3606
3607            if self.withdrawalLimitsFile:
3608                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3609                    fH.write(infoText)
3610
3611                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3612
3613        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3615    def RequestAccounts(self) -> dict:
3616        """
3617        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3618
3619        See also:
3620        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3621        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3622        - `OverviewUserInfo()` method
3623
3624        :return: dict with raw data from server that contains accounts info. Example of dict:
3625                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
3626                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
3627                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
3628                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
3629        """
3630        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
3631
3632        self.body = str({})
3633        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
3634        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
3635
3636        if self.moreDebug:
3637            uLogger.debug("Records about available accounts successfully received")
3638
3639        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
3641    def RequestUserInfo(self) -> dict:
3642        """
3643        Method for requesting common user's information.
3644
3645        See also:
3646        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
3647        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
3648        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
3649        - `OverviewUserInfo()` method
3650
3651        :return: dict with raw data from server that contains user's information. Example of dict:
3652                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
3653                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
3654        """
3655        uLogger.debug("Requesting common user's information. Wait, please...")
3656
3657        self.body = str({})
3658        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
3659        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
3660
3661        if self.moreDebug:
3662            uLogger.debug("Records about current user successfully received")
3663
3664        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
3666    def RequestMarginStatus(self, accountId: str = None) -> dict:
3667        """
3668        Method for requesting margin calculation for defined account ID.
3669
3670        See also:
3671        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
3672        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
3673        - `OverviewUserInfo()` method
3674
3675        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
3676        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
3677                 Example of responses:
3678                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
3679                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
3680                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
3681                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
3682                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
3683                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
3684        """
3685        if accountId is None or not accountId:
3686            if self.accountId is None or not self.accountId:
3687                uLogger.error("Variable `accountId` must be defined for using this method!")
3688                raise Exception("Account ID required")
3689
3690            else:
3691                accountId = self.accountId  # use `self.accountId` (main ID) by default
3692
3693        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
3694
3695        self.body = str({"accountId": accountId})
3696        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
3697        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
3698
3699        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
3700            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
3701            rawMargin = {}
3702
3703        else:
3704            if self.moreDebug:
3705                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
3706
3707        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
3709    def RequestTariffLimits(self) -> dict:
3710        """
3711        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
3712
3713        See also:
3714        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
3715        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
3716        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
3717        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
3718        - `OverviewUserInfo()` method
3719
3720        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
3721                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
3722                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
3723        """
3724        uLogger.debug("Requesting limits of current tariff. Wait, please...")
3725
3726        self.body = str({})
3727        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
3728        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3729
3730        if self.moreDebug:
3731            uLogger.debug("Records with limits of current tariff successfully received")
3732
3733        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
3735    def RequestBondCoupons(self, iJSON: dict) -> dict:
3736        """
3737        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
3738        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
3739        All dates are in UTC timezone.
3740
3741        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
3742        Documentation:
3743        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
3744        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
3745
3746        See also: `ExtendBondsData()`.
3747
3748        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]`
3749                      If raw iJSON is not data of bond then server returns an error [400] with message:
3750                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
3751        :return: dictionary with bond payment calendar. Response example
3752                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
3753                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
3754                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
3755                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
3756        """
3757        if iJSON["figi"] is None or not iJSON["figi"]:
3758            uLogger.error("FIGI must be defined for using this method!")
3759            raise Exception("FIGI required")
3760
3761        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
3762        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
3763
3764        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
3765            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
3766            self.figi,
3767            startDate,
3768            endDate,
3769        ))
3770
3771        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
3772        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
3773        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
3774
3775        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
3776            uLogger.warning("Instrument type is not bond!")
3777
3778        else:
3779            if self.moreDebug:
3780                uLogger.debug("Records about bond payment calendar successfully received")
3781
3782        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self.ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
3784    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
3785        """
3786        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
3787        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
3788        coupon yields, current yields and some statistics etc.
3789
3790        WARNING! This is too long operation if a lot of bonds requested from broker server.
3791
3792        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
3793
3794        :param instruments: list of strings with tickers or FIGIs.
3795        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
3796                     for further used by data scientists or stock analytics.
3797        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
3798                 In XLSX-file and Pandas DataFrame fields mean:
3799                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
3800                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
3801        """
3802        if instruments is None or not instruments:
3803            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3804            raise Exception("Ticker or FIGI required")
3805
3806        if isinstance(instruments, str):
3807            instruments = [instruments]
3808
3809        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3810
3811        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
3812
3813        iCount = len(uniqueInstruments)
3814        tooLong = iCount >= 20
3815        if tooLong:
3816            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
3817
3818        bonds = None
3819        for i, self.figi in enumerate(uniqueInstruments):
3820            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
3821
3822            if "type" in instrument.keys() and instrument["type"] == "Bonds":
3823                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
3824                rawBond = self.SearchByFIGI(requestPrice=True)
3825
3826                # Widen raw data with UTC current time (iData["actualDateTime"]):
3827                actualDate = datetime.now(tzutc())
3828                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
3829
3830                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
3831                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
3832
3833                # Replace some values with human-readable:
3834                iData["nominalCurrency"] = iData["nominal"]["currency"]
3835                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
3836                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
3837                iData["aciCurrency"] = iData["aciValue"]["currency"]
3838                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
3839                iData["issueSize"] = int(iData["issueSize"])
3840                iData["issueSizePlan"] = int(iData["issueSizePlan"])
3841                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
3842                iData["step"] = iData["step"] if "step" in iData.keys() else 0
3843                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
3844                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
3845                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
3846                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
3847                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
3848                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
3849                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
3850
3851                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
3852                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
3853                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
3854                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
3855                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
3856                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
3857                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
3858                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
3859                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
3860                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
3861                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
3862
3863                # Widen raw data with calendar data from `rawCalendar` values:
3864                calendarData = []
3865                if "events" in iData["rawCalendar"].keys():
3866                    for item in iData["rawCalendar"]["events"]:
3867                        calendarData.append({
3868                            "couponDate": item["couponDate"],
3869                            "couponNumber": int(item["couponNumber"]),
3870                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
3871                            "payCurrency": item["payOneBond"]["currency"],
3872                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
3873                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
3874                            "couponStartDate": item["couponStartDate"],
3875                            "couponEndDate": item["couponEndDate"],
3876                            "couponPeriod": item["couponPeriod"],
3877                        })
3878
3879                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
3880                    if "maturityDate" not in iData.keys():
3881                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
3882
3883                # Widen raw data with Coupon Rate.
3884                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
3885                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
3886                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
3887                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
3888
3889                # Widen raw data with Yield to Maturity (YTM) on current date.
3890                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
3891                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
3892                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
3893                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
3894                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
3895                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
3896
3897                iData["calendar"] = calendarData  # adds calendar at the end
3898
3899                # Remove not used data:
3900                iData.pop("uid")
3901                iData.pop("positionUid")
3902                iData.pop("currentPrice")
3903                iData.pop("rawCalendar")
3904
3905                colNames = list(iData.keys())
3906                if bonds is None:
3907                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
3908
3909                else:
3910                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
3911
3912            else:
3913                uLogger.warning("Instrument is not a bond!")
3914
3915            processed = round(100 * (i + 1) / iCount, 1)
3916            if tooLong and processed % 5 == 0:
3917                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
3918
3919            else:
3920                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
3921
3922        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
3923
3924        # Saving bonds from Pandas DataFrame to XLSX sheet:
3925        if xlsx and self.bondsXLSXFile:
3926            with pd.ExcelWriter(
3927                    path=self.bondsXLSXFile,
3928                    date_format=TKS_DATE_FORMAT,
3929                    datetime_format=TKS_DATE_TIME_FORMAT,
3930                    mode="w",
3931            ) as writer:
3932                bonds.to_excel(
3933                    writer,
3934                    sheet_name="Extended bonds data",
3935                    index=True,
3936                    encoding="UTF-8",
3937                    freeze_panes=(1, 1),
3938                )  # saving as XLSX-file with freeze first row and column as headers
3939
3940            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
3941
3942        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
3944    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
3945        """
3946        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
3947
3948        WARNING! This is too long operation if a lot of bonds requested from broker server.
3949
3950        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
3951
3952        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
3953                        extended information about bonds: main info, current prices, bond payment calendar,
3954                        coupon yields, current yields and some statistics etc.
3955                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
3956        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
3957                     for further used by data scientists or stock analytics.
3958        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
3959        """
3960        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
3961            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
3962
3963        uLogger.debug("Generating bond payments calendar data. Wait, please...")
3964
3965        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
3966        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
3967        calendar = None
3968        for bond in extBonds.iterrows():
3969            for item in bond[1]["calendar"]:
3970                cData = {
3971                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
3972                    "couponDate": item["couponDate"],
3973                    "figi": bond[1]["figi"],
3974                    "ticker": bond[1]["ticker"],
3975                    "name": bond[1]["name"],
3976                    "couponNumber": item["couponNumber"],
3977                    "payOneBond": item["payOneBond"],
3978                    "payCurrency": item["payCurrency"],
3979                    "couponType": item["couponType"],
3980                    "couponPeriod": item["couponPeriod"],
3981                    "fixDate": item["fixDate"],
3982                    "couponStartDate": item["couponStartDate"],
3983                    "couponEndDate": item["couponEndDate"],
3984                }
3985
3986                if calendar is None:
3987                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
3988
3989                else:
3990                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
3991
3992        if calendar is not None:
3993            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
3994
3995            # Saving calendar from Pandas DataFrame to XLSX sheet:
3996            if xlsx:
3997                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
3998
3999                with pd.ExcelWriter(
4000                        path=xlsxCalendarFile,
4001                        date_format=TKS_DATE_FORMAT,
4002                        datetime_format=TKS_DATE_TIME_FORMAT,
4003                        mode="w",
4004                ) as writer:
4005                    humanReadable = calendar.copy(deep=True)
4006                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4007                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4008                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4009                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4010                    humanReadable.columns = colNames  # human-readable column names
4011
4012                    humanReadable.to_excel(
4013                        writer,
4014                        sheet_name="Bond payments calendar",
4015                        index=False,
4016                        encoding="UTF-8",
4017                        freeze_panes=(1, 2),
4018                    )  # saving as XLSX-file with freeze first row and column as headers
4019
4020                    del humanReadable  # release df in memory
4021
4022                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4023
4024        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4026    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4027        """
4028        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4029        Also, creates Markdown file with calendar data, `calendar.md` by default.
4030
4031        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4032
4033        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4034                        extended information about bonds: main info, current prices, bond payment calendar,
4035                        coupon yields, current yields and some statistics etc.
4036                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4037        :param show: if `True` then also printing bonds payment calendar to the console,
4038                     otherwise save to file `calendarFile` only. `False` by default.
4039        :return: multilines text in Markdown format with bonds payment calendar as a table.
4040        """
4041        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4042            extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False)
4043
4044        infoText = "# Bond payments calendar\n\n"
4045
4046        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4047
4048        if not (calendar is None or calendar.empty):
4049            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4050
4051            info = [
4052                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4053                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4054            ]
4055
4056            newMonth = False
4057            notOneBond = calendar["figi"].nunique() > 1
4058            for i, bond in enumerate(calendar.iterrows()):
4059                if newMonth and notOneBond:
4060                    info.append(splitLine)
4061
4062                info.append(
4063                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4064                        "  √" if bond[1]["paid"] else "  —",
4065                        bond[1]["couponDate"].split("T")[0],
4066                        bond[1]["figi"],
4067                        bond[1]["ticker"],
4068                        bond[1]["couponNumber"],
4069                        "{} {}".format(
4070                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4071                            bond[1]["payCurrency"],
4072                        ),
4073                        bond[1]["couponType"],
4074                        bond[1]["couponPeriod"],
4075                        bond[1]["fixDate"].split("T")[0],
4076                    )
4077                )
4078
4079                if i < len(calendar.values) - 1:
4080                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4081                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4082                    newMonth = False if curDate.month == nextDate.month else True
4083
4084                else:
4085                    newMonth = False
4086
4087            infoText += "".join(info)
4088
4089            if show:
4090                uLogger.info("{}".format(infoText))
4091
4092            if self.calendarFile is not None:
4093                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4094                    fH.write(infoText)
4095
4096                uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4097
4098        else:
4099            infoText += "No data\n"
4100
4101        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4103    def OverviewAccounts(self, show: bool = False) -> dict:
4104        """
4105        Method for parsing and show simple table with all available user accounts.
4106
4107        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4108
4109        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4110        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4111                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4112                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4113                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4114                                                        "closed": "—", "access": "Full access" }, ...}}`
4115        """
4116        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4117
4118        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4119        accounts = {
4120            item["id"]: {
4121                "type": TKS_ACCOUNT_TYPES[item["type"]],
4122                "name": item["name"],
4123                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4124                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4125                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4126                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4127            } for item in rawAccounts["accounts"]
4128        }
4129
4130        # Raw and parsed data with some fields replaced in "stat" section:
4131        view = {
4132            "rawAccounts": rawAccounts,
4133            "stat": accounts,
4134        }
4135
4136        # --- Prepare simple text table with only accounts data in human-readable format:
4137        if show:
4138            info = [
4139                "# User accounts\n\n",
4140                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4141                "| Account ID   | Type                      | Status                    | Name                           |\n",
4142                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4143            ]
4144
4145            for account in view["stat"].keys():
4146                info.extend([
4147                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4148                        account,
4149                        view["stat"][account]["type"],
4150                        view["stat"][account]["status"],
4151                        view["stat"][account]["name"],
4152                    )
4153                ])
4154
4155            infoText = "".join(info)
4156
4157            uLogger.info(infoText)
4158
4159            if self.userAccountsFile:
4160                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4161                    fH.write(infoText)
4162
4163                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4164
4165        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4167    def OverviewUserInfo(self, show: bool = False) -> dict:
4168        """
4169        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4170
4171        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4172
4173        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4174        :return: dict with raw parsed data from server and some calculated statistics about it.
4175        """
4176        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4177        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4178        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4179        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4180        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4181        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4182
4183        # This is dict with parsed common user data:
4184        userInfo = {
4185            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4186            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4187            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4188            "tariff": rawUserInfo["tariff"],
4189        }
4190
4191        # This is an array of dict with parsed margin statuses for every account IDs:
4192        margins = {}
4193        for accountId in accounts.keys():
4194            if rawMargins[accountId]:
4195                margins[accountId] = {
4196                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4197                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4198                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4199                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4200                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4201                    "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4202                }
4203
4204            else:
4205                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4206
4207        unary = {}  # unary-connection limits
4208        for item in rawTariffLimits["unaryLimits"]:
4209            if item["limitPerMinute"] in unary.keys():
4210                unary[item["limitPerMinute"]].extend(item["methods"])
4211
4212            else:
4213                unary[item["limitPerMinute"]] = item["methods"]
4214
4215        stream = {}  # stream-connection limits
4216        for item in rawTariffLimits["streamLimits"]:
4217            if item["limit"] in stream.keys():
4218                stream[item["limit"]].extend(item["streams"])
4219
4220            else:
4221                stream[item["limit"]] = item["streams"]
4222
4223        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4224        limits = {
4225            "unary": unary,
4226            "stream": stream,
4227        }
4228
4229        # Raw and parsed data as an output result:
4230        view = {
4231            "rawUserInfo": rawUserInfo,
4232            "rawAccounts": rawAccounts,
4233            "rawMargins": rawMargins,
4234            "rawTariffLimits": rawTariffLimits,
4235            "stat": {
4236                "userInfo": userInfo,
4237                "accounts": accounts,
4238                "margins": margins,
4239                "limits": limits,
4240            },
4241        }
4242
4243        # --- Prepare text table with user information in human-readable format:
4244        if show:
4245            info = [
4246                "# Full user information\n\n",
4247                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4248                "## Common information\n\n",
4249                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4250                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4251                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4252                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4253                "\n## User accounts\n\n",
4254            ]
4255
4256            for account in view["stat"]["accounts"].keys():
4257                info.extend([
4258                    "### ID: [{}]\n\n".format(account),
4259                    "| Parameters           | Values                                                       |\n",
4260                    "|----------------------|--------------------------------------------------------------|\n",
4261                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4262                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4263                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4264                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4265                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4266                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4267                ])
4268
4269                if margins[account]:
4270                    info.extend([
4271                        "| Margin status:       | Enabled                                                      |\n",
4272                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4273                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4274                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4275                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4276                        "| - Missing funds:     | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])),
4277                    ])
4278
4279                else:
4280                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4281
4282            info.extend([
4283                "\n## Current user tariff limits\n",
4284                "\nSee also:\n",
4285                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4286                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4287                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4288                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4289                "\n### Unary limits\n",
4290            ])
4291
4292            if unary:
4293                for key, values in sorted(unary.items()):
4294                    info.append("\n* Max requests per minute: {}\n".format(key))
4295
4296                    for value in values:
4297                        info.append("  - {}\n".format(value))
4298
4299            else:
4300                info.append("\nNot available\n")
4301
4302            info.append("\n### Stream limits\n")
4303
4304            if stream:
4305                for key, values in sorted(stream.items()):
4306                    info.append("\n* Max stream connections: {}\n".format(key))
4307
4308                    for value in values:
4309                        info.append("  - {}\n".format(value))
4310
4311            else:
4312                info.append("\nNot available\n")
4313
4314            infoText = "".join(info)
4315
4316            uLogger.info(infoText)
4317
4318            if self.userInfoFile:
4319                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4320                    fH.write(infoText)
4321
4322                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4323
4324        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4327class Args:
4328    """
4329    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4330    """
4331    def __init__(self, **kwargs):
4332        self.__dict__.update(kwargs)
4333
4334    def __getattr__(self, item):
4335        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4331    def __init__(self, **kwargs):
4332        self.__dict__.update(kwargs)
def ParseArgs()
4338def ParseArgs():
4339    """This function get and parse command line keys."""
4340    parser = ArgumentParser()  # command-line string parser
4341
4342    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4343    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4344
4345    # --- options:
4346
4347    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4348    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4349    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4350
4351    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4352    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4353
4354    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4355    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4356
4357    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4358
4359    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4360    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4361    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4362
4363    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4364    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4365
4366    # --- commands:
4367
4368    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4369
4370    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4371    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4372    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4373    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4374    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4375    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4376    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4377    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4378
4379    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4380    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4381    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4382    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4383    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4384    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4385
4386    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4387    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4388    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4389    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4390
4391    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4392    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4393    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4394
4395    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4396    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4397    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4398    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4399    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4400    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4401    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4402
4403    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4404    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4405    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4406    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4407    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.")
4408
4409    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4410    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4411    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4412
4413    cmdArgs = parser.parse_args()
4414    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs)
4417def Main(**kwargs):
4418    """
4419    Main function for work with TKSBrokerAPI in the console.
4420
4421    See examples:
4422    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4423    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4424    """
4425    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4426
4427    if args.debug_level:
4428        uLogger.level = 10  # always debug level by default
4429        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4430
4431    exitCode = 0
4432    start = datetime.now(tzutc())
4433    uLogger.debug("=-" * 50)
4434    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4435        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4436        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4437    ))
4438
4439    # trying to calculate full current version:
4440    buildVersion = __version__
4441    try:
4442        v = version("tksbrokerapi")
4443        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4444
4445    except Exception:
4446        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4447
4448    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4449    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4450
4451    try:
4452        if args.version:
4453            print("TKSBrokerAPI {}".format(buildVersion))
4454            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4455
4456        else:
4457            # Init class for trading with Tinkoff Broker:
4458            trader = TinkoffBrokerServer(
4459                token=args.token,
4460                accountId=args.account_id,
4461                useCache=not args.no_cache,
4462            )
4463
4464            # --- set some options:
4465
4466            if args.more:
4467                trader.moreDebug = True
4468                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4469
4470            if args.ticker:
4471                ticker = args.ticker.upper()  # Tickers may be upper case only
4472
4473                if ticker in trader.aliasesKeys:
4474                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4475
4476                else:
4477                    trader.ticker = ticker
4478
4479            if args.figi:
4480                trader.figi = args.figi.upper()  # FIGIs may be upper case only
4481
4482            if args.depth is not None:
4483                trader.depth = args.depth
4484
4485            # --- do one command:
4486
4487            if args.list:
4488                if args.output is not None:
4489                    trader.instrumentsFile = args.output
4490
4491                trader.ShowInstrumentsInfo(show=True)
4492
4493            elif args.list_xlsx:
4494                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4495
4496            elif args.bonds_xlsx is not None:
4497                if args.output is not None:
4498                    trader.bondsXLSXFile = args.output
4499
4500                if len(args.bonds_xlsx) == 0:
4501                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4502
4503                else:
4504                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4505
4506            elif args.search:
4507                if args.output is not None:
4508                    trader.searchResultsFile = args.output
4509
4510                trader.SearchInstruments(pattern=args.search[0], show=True)
4511
4512            elif args.info:
4513                if not (args.ticker or args.figi):
4514                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4515                    raise Exception("Ticker or FIGI required")
4516
4517                if args.output is not None:
4518                    trader.infoFile = args.output
4519
4520                if args.ticker:
4521                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4522
4523                else:
4524                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4525
4526            elif args.calendar is not None:
4527                if args.output is not None:
4528                    trader.calendarFile = args.output
4529
4530                if len(args.calendar) == 0:
4531                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4532
4533                else:
4534                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4535
4536                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4537
4538            elif args.price:
4539                if not (args.ticker or args.figi):
4540                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4541                    raise Exception("Ticker or FIGI required")
4542
4543                trader.GetCurrentPrices(show=True)
4544
4545            elif args.prices is not None:
4546                if args.output is not None:
4547                    trader.pricesFile = args.output
4548
4549                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4550
4551            elif args.overview:
4552                if args.output is not None:
4553                    trader.overviewFile = args.output
4554
4555                trader.Overview(show=True, details="full")
4556
4557            elif args.overview_digest:
4558                if args.output is not None:
4559                    trader.overviewDigestFile = args.output
4560
4561                trader.Overview(show=True, details="digest")
4562
4563            elif args.overview_positions:
4564                if args.output is not None:
4565                    trader.overviewPositionsFile = args.output
4566
4567                trader.Overview(show=True, details="positions")
4568
4569            elif args.overview_orders:
4570                if args.output is not None:
4571                    trader.overviewOrdersFile = args.output
4572
4573                trader.Overview(show=True, details="orders")
4574
4575            elif args.overview_analytics:
4576                if args.output is not None:
4577                    trader.overviewAnalyticsFile = args.output
4578
4579                trader.Overview(show=True, details="analytics")
4580
4581            elif args.overview_calendar:
4582                if args.output is not None:
4583                    trader.overviewAnalyticsFile = args.output
4584
4585                trader.Overview(show=True, details="calendar")
4586
4587            elif args.deals is not None:
4588                if args.output is not None:
4589                    trader.reportFile = args.output
4590
4591                if 0 <= len(args.deals) < 3:
4592                    trader.Deals(
4593                        start=args.deals[0] if len(args.deals) >= 1 else None,
4594                        end=args.deals[1] if len(args.deals) == 2 else None,
4595                        show=True,  # Always show deals report in console
4596                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
4597                    )
4598
4599                else:
4600                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4601                    raise Exception("Incorrect value")
4602
4603            elif args.history is not None:
4604                if args.output is not None:
4605                    trader.historyFile = args.output
4606
4607                if 0 <= len(args.history) < 3:
4608                    dataReceived = trader.History(
4609                        start=args.history[0] if len(args.history) >= 1 else None,
4610                        end=args.history[1] if len(args.history) == 2 else None,
4611                        interval="hour" if args.interval is None or not args.interval else args.interval,
4612                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
4613                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
4614                        show=True,  # shows all downloaded candles in console
4615                    )
4616
4617                    if args.render_chart is not None and dataReceived is not None:
4618                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4619
4620                        trader.ShowHistoryChart(
4621                            candles=dataReceived,
4622                            interact=iChart,
4623                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4624                        )
4625
4626                else:
4627                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
4628                    raise Exception("Incorrect value")
4629
4630            elif args.load_history is not None:
4631                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
4632
4633                if args.render_chart is not None and histData is not None:
4634                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
4635                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
4636
4637                    trader.ShowHistoryChart(
4638                        candles=histData,
4639                        interact=iChart,
4640                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
4641                    )
4642
4643            elif args.trade is not None:
4644                if 1 <= len(args.trade) <= 5:
4645                    trader.Trade(
4646                        operation=args.trade[0],
4647                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
4648                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
4649                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
4650                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
4651                    )
4652
4653                else:
4654                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4655
4656            elif args.buy is not None:
4657                if 0 <= len(args.buy) <= 4:
4658                    trader.Buy(
4659                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
4660                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
4661                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
4662                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
4663                    )
4664
4665                else:
4666                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4667
4668            elif args.sell is not None:
4669                if 0 <= len(args.sell) <= 4:
4670                    trader.Sell(
4671                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
4672                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
4673                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
4674                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
4675                    )
4676
4677                else:
4678                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4679
4680            elif args.order:
4681                if 4 <= len(args.order) <= 7:
4682                    trader.Order(
4683                        operation=args.order[0],
4684                        orderType=args.order[1],
4685                        lots=int(args.order[2]),
4686                        targetPrice=float(args.order[3]),
4687                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
4688                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
4689                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
4690                    )
4691
4692                else:
4693                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
4694
4695            elif args.buy_limit:
4696                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
4697
4698            elif args.sell_limit:
4699                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
4700
4701            elif args.buy_stop:
4702                if 2 <= len(args.buy_stop) <= 7:
4703                    trader.BuyStop(
4704                        lots=int(args.buy_stop[0]),
4705                        targetPrice=float(args.buy_stop[1]),
4706                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
4707                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
4708                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
4709                    )
4710
4711                else:
4712                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
4713
4714            elif args.sell_stop:
4715                if 2 <= len(args.sell_stop) <= 7:
4716                    trader.SellStop(
4717                        lots=int(args.sell_stop[0]),
4718                        targetPrice=float(args.sell_stop[1]),
4719                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
4720                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
4721                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
4722                    )
4723
4724                else:
4725                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
4726
4727            # elif args.buy_order_grid is not None:
4728            #     # update order grid work with api v2
4729            #     if len(args.buy_order_grid) == 2:
4730            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
4731            #
4732            #         for order in orderParams:
4733            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
4734            #
4735            #     else:
4736            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4737            #
4738            # elif args.sell_order_grid is not None:
4739            #     # update order grid work with api v2
4740            #     if len(args.sell_order_grid) >= 2:
4741            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
4742            #
4743            #         for order in orderParams:
4744            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
4745            #
4746            #     else:
4747            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
4748
4749            elif args.close_order is not None:
4750                trader.CloseOrders(args.close_order)  # close only one order
4751
4752            elif args.close_orders is not None:
4753                trader.CloseOrders(args.close_orders)  # close list of orders
4754
4755            elif args.close_trade:
4756                if not (args.ticker or args.figi):
4757                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4758                    raise Exception("Ticker or FIGI required")
4759
4760                if args.ticker:
4761                    trader.CloseTrades([args.ticker])  # close only one trade by ticker (priority)
4762
4763                else:
4764                    trader.CloseTrades([args.figi])  # close only one trade by FIGI
4765
4766            elif args.close_trades is not None:
4767                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
4768
4769            elif args.close_all is not None:
4770                trader.CloseAll(*args.close_all)
4771
4772            elif args.limits:
4773                if args.output is not None:
4774                    trader.withdrawalLimitsFile = args.output
4775
4776                trader.OverviewLimits(show=True)
4777
4778            elif args.user_info:
4779                if args.output is not None:
4780                    trader.userInfoFile = args.output
4781
4782                trader.OverviewUserInfo(show=True)
4783
4784            elif args.account:
4785                if args.output is not None:
4786                    trader.userAccountsFile = args.output
4787
4788                trader.OverviewAccounts(show=True)
4789
4790            else:
4791                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
4792                raise Exception("There is no command to execute")
4793
4794    except Exception:
4795        trace = tb.format_exc()
4796        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
4797            if e in trace:
4798                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
4799                break
4800
4801        uLogger.debug(trace)
4802        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
4803        exitCode = 255  # an error occurred, must be open a ticket for this issue
4804
4805    finally:
4806        finish = datetime.now(tzutc())
4807
4808        if exitCode == 0:
4809            if args.more:
4810                uLogger.debug("All operations were finished success (summary code is 0).")
4811
4812        else:
4813            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
4814                os.path.abspath(uLog.defaultLogFile), exitCode,
4815            ))
4816
4817        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
4818        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
4819            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4820            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4821        ))
4822        uLogger.debug("=-" * 50)
4823
4824        if not kwargs:
4825            sys.exit(exitCode)
4826
4827        else:
4828            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: